Compare commits

..

28 Commits

Author SHA1 Message Date
Viktor Scharf
75bc05ae02 disable running ci with watch fs when full-ci 2025-11-24 16:39:39 +01:00
Ralf Haferkamp
1727aa1162 devtools: add deployment for multi-tenancy 2025-11-24 11:57:55 +01:00
opencloudeu
a181049508 [tx] updated from transifex 2025-11-24 00:02:52 +00:00
opencloudeu
718e86b06e [tx] updated from transifex 2025-11-23 00:03:05 +00:00
Benedikt Kulmann
487a2a0aa6 fix: add update server to default csp rules (#1875)
* fix: add update server to default csp rules

* adapt tests

---------

Co-authored-by: Viktor Scharf <v.scharf@opencloud.eu>
2025-11-21 17:13:22 +01:00
Alex
a496b6f46b fix: add missing capability flag support-radicale (#1891) 2025-11-21 15:39:46 +01:00
Jörn Friedrich Dreyer
c1fcf71d42 Merge pull request #1890 from opencloud-eu/fix-opensearch-client-cert
fix opensearch client certificate

well ... technically it is not a fix. We expected the certificate on the CLI to be in PEM format. so, it would have worked if you used sth. like:
```console
export SEARCH_ENGINE_OPEN_SEARCH_CLIENT_CA_CERT="-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKJ...
...
-----END CERTIFICATE-----"
```

which was different than all our other cert env vars, which take a path.
2025-11-21 15:15:48 +01:00
Viktor Scharf
d1fa52b603 revaBump-getting#428 (#1887) 2025-11-21 13:11:10 +01:00
Jörn Friedrich Dreyer
538e8141b2 fix opensearch client certificate
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2025-11-21 12:23:07 +01:00
opencloudeu
ba71d2978a [tx] updated from transifex 2025-11-21 00:03:13 +00:00
dependabot[bot]
4ae0951f5f build(deps): bump github.com/blevesearch/bleve/v2 from 2.5.4 to 2.5.5
Bumps [github.com/blevesearch/bleve/v2](https://github.com/blevesearch/bleve) from 2.5.4 to 2.5.5.
- [Release notes](https://github.com/blevesearch/bleve/releases)
- [Commits](https://github.com/blevesearch/bleve/compare/v2.5.4...v2.5.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 17:41:01 +01:00
Ralf Haferkamp
e85d8effc1 Bump reva
Fixes: #1871, #1867
2025-11-20 14:58:30 +01:00
Michael Flemming
90e4127227 Merge pull request #1881 from opencloud-eu/fix_rc_latest_overwrite 2025-11-20 14:14:41 +01:00
dependabot[bot]
28148d02bd build(deps): bump github.com/olekukonko/tablewriter from 1.1.0 to 1.1.1
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.0 to 1.1.1.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 11:50:20 +01:00
Michael 'Flimmy' Flemming
205ffbbe83 add logic to skip latest tag for pre-releases 2025-11-20 11:27:57 +01:00
Viktor Scharf
9d173f0ea6 api-tests: delete spaces before users (#1877)
* delete spaces before users

* delete users after deleting spaces

* fix
2025-11-20 09:12:48 +01:00
Thomas Schweiger
6161e40d43 Merge pull request #1859 from opencloud-eu/fix/create-missing-readme-files
fix: add missing service README.md files with basic description
2025-11-19 19:16:16 +01:00
Christian Richter
95b19c7e33 Merge pull request #1617 from dragonchaser/merge-csp-configs
load two yaml configs
2025-11-19 14:20:43 +01:00
Christian Richter
97ee9b36a5 incorporate requested changes
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-11-19 09:38:16 +01:00
Christian Richter
f9807f9f3a actually load overrideyaml
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-11-19 09:38:16 +01:00
Christian Richter
8007e8a269 add ability to completely override csp config
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-11-19 09:38:16 +01:00
Christian Richter
63603679a5 remove obsolete comment
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-11-19 09:38:16 +01:00
Christian Richter
16f9667fe8 adapt tests & deepmerge
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-11-19 09:38:16 +01:00
Christian Richter
d16524510a adapt tests
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-11-19 09:38:16 +01:00
Christian Richter
20b903b32d load two yaml configs
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-11-19 09:38:16 +01:00
Michael Barz
54a38e37c6 Apply suggestions from code review
Co-authored-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2025-11-17 10:43:44 +01:00
Jörn Friedrich Dreyer
4de25fdb5e Apply suggestions from code review 2025-11-17 10:41:17 +01:00
Thomas Schweiger
aa2da8372b fix: add missing service README.md files with basic description 2025-11-14 12:24:01 +01:00
239 changed files with 23519 additions and 26263 deletions

View File

@@ -608,8 +608,8 @@ def testPipelines(ctx):
pipelines += apiTests(ctx)
enable_watch_fs = [False]
if ctx.build.event == "cron" or "full-ci" in ctx.build.title.lower():
enable_watch_fs.append(True)
if ctx.build.event == "cron" in ctx.build.title.lower():
enable_watch_fs.append(True)
for run_with_watch_fs_enabled in enable_watch_fs:
pipelines += e2eTestPipeline(ctx, run_with_watch_fs_enabled) + multiServiceE2ePipeline(ctx, run_with_watch_fs_enabled)
@@ -994,7 +994,7 @@ def localApiTestPipeline(ctx):
with_remote_php = [True]
enable_watch_fs = [False]
if ctx.build.event == "cron" or "full-ci" in ctx.build.title.lower():
if ctx.build.event == "cron" in ctx.build.title.lower():
with_remote_php.append(False)
enable_watch_fs.append(True)
@@ -1310,7 +1310,7 @@ def apiTests(ctx):
with_remote_php = [True]
enable_watch_fs = [False]
if ctx.build.event == "cron" or "full-ci" in ctx.build.title.lower():
if ctx.build.event == "cron" in ctx.build.title.lower():
with_remote_php.append(False)
enable_watch_fs.append(True)
@@ -1654,6 +1654,16 @@ def dockerRelease(ctx, repo, build_type):
"VERSION": "%s" % (ctx.build.ref.replace("refs/tags/", "") if ctx.build.event == "tag" else "daily"),
}
# if no additional tag is given, the build-plugin adds latest
hard_tag = "daily"
if ctx.build.event == "tag":
tag_version = ctx.build.ref.replace("refs/tags/", "")
tag_parts = tag_version.split("-")
# if a tag has something appended with "-" i.e. alpha, beta, rc1...
# set the entire string as tag, else leave empty to autotag with latest
hard_tag = tag_version if len(tag_parts) > 1 else ""
depends_on = getPipelineNames(getGoBinForTesting(ctx))
if ctx.build.event == "tag":
@@ -1672,7 +1682,7 @@ def dockerRelease(ctx, repo, build_type):
"platforms": "linux/amd64", # do dry run only on the native platform
"repo": "%s,quay.io/%s" % (repo, repo),
"auto_tag": False if build_type == "daily" else True,
"tag": "daily" if build_type == "daily" else "",
"tag": hard_tag,
"default_tag": "daily",
"dockerfile": "opencloud/docker/Dockerfile.multiarch",
"build_args": build_args,

View File

@@ -0,0 +1,161 @@
## Basic Settings ##
# Define the docker compose log driver used.
# Defaults to local
LOG_DRIVER=
# If you're on an internet facing server, comment out following line.
# It skips certificate validation for various parts of OpenCloud and is
# needed when self signed certificates are used.
INSECURE=true
## Features ##
COMPOSE_FILE=docker-compose.yml:traefik.yml:keycloak.yml:ldap-server.yml
## Traefik Settings ##
# Note: Traefik is always enabled and can't be disabled.
# Serve Traefik dashboard.
# Defaults to "false".
TRAEFIK_DASHBOARD=
# Domain of Traefik, where you can find the dashboard.
# Defaults to "traefik.opencloud.test"
TRAEFIK_DOMAIN=
# Basic authentication for the traefik dashboard.
# Defaults to user "admin" and password "admin" (written as: "admin:$2y$05$KDHu3xq92SPaO3G8Ybkc7edd51pPLJcG1nWk3lmlrIdANQ/B6r5pq").
# To create user:password pair, it's possible to use this command:
# echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
TRAEFIK_BASIC_AUTH_USERS=
# Email address for obtaining LetsEncrypt certificates.
# Needs only be changed if this is a public facing server.
TRAEFIK_ACME_MAIL=
# Set to the following for testing to check the certificate process:
# "https://acme-staging-v02.api.letsencrypt.org/directory"
# With staging configured, there will be an SSL error in the browser.
# When certificates are displayed and are emitted by # "Fake LE Intermediate X1",
# the process went well and the envvar can be reset to empty to get valid certificates.
TRAEFIK_ACME_CASERVER=
# Enable the Traefik ACME (Automatic Certificate Management Environment) for automatic SSL certificate management.
TRAEFIK_SERVICES_TLS_CONFIG="tls.certresolver=letsencrypt"
# Enable Traefik to use local certificates.
#TRAEFIK_SERVICES_TLS_CONFIG="tls=true"
# You also need to provide a config file in ./config/traefik/dynamic/certs.yml
# Example:
# cat ./config/traefik/dynamic/certs.yml
# tls:
# certificates:
# - certFile: /certs/opencloud.test.crt
# keyFile: /certs/opencloud.test.key
# stores:
# - default
#
# The certificates need to copied into ./certs/, the absolute path inside the container is /certs/.
# You can also use TRAEFIK_CERTS_DIR=/path/on/host to set the path to the certificates directory.
# Enable the access log for Traefik by setting the following variable to true.
TRAEFIK_ACCESS_LOG=
# Configure the log level for Traefik.
# Possible values are "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" and "PANIC". Default is "ERROR".
TRAEFIK_LOG_LEVEL=
## OpenCloud Settings ##
# The opencloud container image.
# For production releases: "opencloudeu/opencloud"
# For rolling releases: "opencloudeu/opencloud-rolling"
# Defaults to production if not set otherwise
OC_DOCKER_IMAGE=opencloudeu/opencloud-rolling
# The openCloud container version.
# Defaults to "latest" and points to the latest stable tag.
OC_DOCKER_TAG=
# Domain of openCloud, where you can find the frontend.
# Defaults to "cloud.opencloud.test"
OC_DOMAIN=
# Demo users should not be created on a production instance,
# because their passwords are public. Defaults to "false".
# If demo users is set to "true", the following user accounts are created automatically:
# alan, mary, margaret, dennis and lynn - the password is 'demo' for all.
DEMO_USERS=
# Admin Password for the OpenCloud admin user.
# NOTE: This is only needed when using the built-in LDAP server (idm).
# If you are using an external LDAP server, the admin password is managed by the LDAP server.
# NOTE: This variable needs to be set before the first start of OpenCloud. Changes to this variable after the first start will be IGNORED.
# If not set, opencloud will not work properly. The container will be restarting.
# After the first initialization, the admin password can only be changed via the OpenCloud User Settings UI or by using the OpenCloud CLI.
# Documentation: https://docs.opencloud.eu/docs/admin/resources/common-issues#-change-admin-password-set-in-env
INITIAL_ADMIN_PASSWORD=
# Define the openCloud loglevel used.
#
LOG_LEVEL=
# Define the kind of logging.
# The default log can be read by machines.
# Set this to true to make the log human readable.
# LOG_PRETTY=true
#
# Define the openCloud storage location. Set the paths for config and data to a local path.
# Ensure that the configuration and data directories are owned by the user and group with ID 1000:1000.
# This matches the default user inside the container and avoids permission issues when accessing files.
# Note that especially the data directory can grow big.
# Leaving it default stores data in docker internal volumes.
# OC_CONFIG_DIR=/your/local/opencloud/config
# OC_DATA_DIR=/your/local/opencloud/data
### Compose Configuration ###
# Path separator for supplemental compose files specified in COMPOSE_FILE.
COMPOSE_PATH_SEPARATOR=:
### Ldap Settings ###
# LDAP is always needed for OpenCloud to store user data as there is no relational database.
# The built-in LDAP server should used for testing purposes or small installations only.
# For production installations, it is recommended to use an external LDAP server.
# We are using OpenLDAP as the default LDAP server because it is proven to be stable and reliable.
# This LDAP configuration is known to work with OpenCloud and provides a blueprint for
# configuring an external LDAP server based on other products like Microsoft Active Directory or other LDAP servers.
#
# Password of LDAP bind user "cn=admin,dc=opencloud,dc=eu". Defaults to "admin"
LDAP_BIND_PASSWORD=
# The LDAP server also creates an openCloud admin user dn: uid=admin,ou=users,dc=opencloud,dc=eu
# The initial password for this user is "admin"
# NOTE: This password can only be set once, if you want to change it later, you have to use the OpenCloud User Settings UI.
# If you changed the password and lost it, you need to execute the following LDAP query to reset it:
# enter the ldap-server container with `docker compose exec ldap-server sh`
# and run the following command to change the password:
# ldappasswd -H ldap://127.0.0.1:1389 -D "cn=admin,dc=opencloud,dc=eu" -W "uid=admin,ou=users,dc=opencloud,dc=eu"
# You will be prompted for the LDAP bind password.
# The output should provide you a new password for the admin user.
### Keycloak Settings ###
# Keycloak is an open-source identity and access management solution.
# We are using Keycloak as the default identity provider on production installations.
# It can be used to federate authentication with other identity providers like
# Microsoft Entra ID, ADFS or other SAML/OIDC providers.
# The use of Keycloak as bridge between OpenCloud and other identity providers creates more control over the
# authentication process, the allowed clients and the session management.
# Keycloak also manages the Role Based Access Control (RBAC) for OpenCloud.
# Keycloak can be used in two different modes:
# 1. Autoprovisioning: New users are automatically created in openCloud when they log in for the first time.
# 2. Shared User Directory: Users are created in Keycloak and can be used in OpenCloud immediately
# because the LDAP server is connected to both Keycloak and OpenCloud.
# Only use one of the two modes at a time.
## Autoprovisioning Mode ##
# Use together with idm/external-idp.yml
# If you want to use a keycloak for local testing, you can use testing/external-keycloak.yml and testing/ldap-manager.yml
# Domain of your Identity Provider.
IDP_DOMAIN=
# IdP Issuer URL, which is used to identify the Identity Provider.
# We need the complete URL, including the protocol (http or https) and the realm.
# Example: "https://keycloak.opencloud.test/realms/openCloud"
IDP_ISSUER_URL=
# Url of the account edit page from your Identity Provider.
IDP_ACCOUNT_URL=
## Shared User Directory Mode ##
# Use together with idm/ldap-keycloak.yml and traefik/ldap-keycloak.yml
# Domain for Keycloak. Defaults to "keycloak.opencloud.test".
KEYCLOAK_DOMAIN=
# Admin user login name. Defaults to "kcadmin".
KEYCLOAK_ADMIN=
# Admin user login password. Defaults to "admin".
KEYCLOAK_ADMIN_PASSWORD=
# Keycloak Database username. Defaults to "keycloak".
KC_DB_USERNAME=
# Keycloak Database password. Defaults to "keycloak".
KC_DB_PASSWORD=

View File

@@ -0,0 +1,49 @@
# Development/Test Deployment for a multi-tenacy setup
The docker compose files in this directory are derived from the
opencloud-compose project and can be used to deploy a Development or Testing
environment for a multi-tenancy setup of OpenCloud. It consists of the
following services:
* `provisioning`: The OpenCloud graph service deployed in a standalone mode. It
is configured to provide the libregraph education API for managing tenants
and users. The `ldap-server`service (see below) is used to store the tenants
and users.
* `ldap-server`: An OpenLDAP server that is used by the provisioning service to
store tenants and users. Used by the OpenCloud services as the user directory
(for looking up users and searching for sharees).
* `keycloak`: The OpenID Connect Provider used for authenticating users. The
pre-loaded realm is configured to add `tenantid` claim into the identity and
access tokens. It's also currently consuming user from the `ldap-server`
(this federation will likely go away in the future and is optional for future
configurations).
* `opencloud`: The OpenCloud configured so that is hides users from different
tenants from each other.
To deploy the setup, run:
```bash
docker compose -f docker-compose.yml -f keycloak.yml -f ldap-server.yml -f traefik.yml up
```
Once deployed you can use the `initialize_users.go` to create a couple of example
tenants and some users in each tenant:
* Tenant `Famous Coders` with users `dennis` and `grace`
* Tenant `Scientists` with users `einstein` and `marie`
The passwords for the users is set to `demo` in keycloak
```
> go run initialize_users.go
Created tenant: Famous Coders with id fc58e19a-3a2a-4afc-90ec-8f94986db340
Created user: Dennis Ritchie with id ee1e14e7-b00b-4eec-8b03-a6bf0e29c77c
Created user: Grace Hopper with id a29f3afd-e4a3-4552-91e8-cc99e26bffce
Created tenant: Scientists with id 18406c53-e2d6-4e83-98b6-a55880eef195
Created user: Albert Einstein with id 12023d37-d6ce-4f19-a318-b70866f265ba
Created user: Marie Curie with id 30c3c825-c37d-4e85-8195-0142e4884872
Setting password for user: grace
Setting password for user: marie
Setting password for user: dennis
Setting password for user: einstein
```

View File

View File

View File

@@ -0,0 +1,63 @@
{
"clientId": "OpenCloudAndroid",
"name": "OpenCloud Android App",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"oc://android.opencloud.eu"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "oc://android.opencloud.eu",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -0,0 +1,64 @@
{
"clientId": "OpenCloudDesktop",
"name": "OpenCloud Desktop Client",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"http://127.0.0.1",
"http://localhost"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "+",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -0,0 +1,63 @@
{
"clientId": "OpenCloudIOS",
"name": "OpenCloud iOS App",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"oc://ios.opencloud.eu"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "oc://ios.opencloud.eu",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -0,0 +1,66 @@
{
"clientId": "Cyberduck",
"name": "Cyberduck",
"description": "File transfer utility client",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"x-cyberduck-action:oauth",
"x-mountainduck-action:oauth"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -0,0 +1,74 @@
{
"clientId": "web",
"name": "OpenCloud Web App",
"description": "",
"rootUrl": "{{OC_URL}}",
"adminUrl": "{{OC_URL}}",
"baseUrl": "",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"{{OC_URL}}/",
"{{OC_URL}}/oidc-callback.html",
"{{OC_URL}}/oidc-silent-redirect.html"
],
"webOrigins": [
"{{OC_URL}}"
],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.url": "{{OC_URL}}/backchannel_logout",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -0,0 +1,8 @@
#!/bin/bash
printenv
# replace openCloud domain and LDAP password in keycloak realm import
mkdir /opt/keycloak/data/import
sed -e "s/cloud.opencloud.test/${OC_DOMAIN}/g" -e "s/ldap-admin-password/${LDAP_ADMIN_PASSWORD:-admin}/g" /opt/keycloak/data/import-dist/openCloud-realm.json > /opt/keycloak/data/import/openCloud-realm.json
# run original docker-entrypoint
/opt/keycloak/bin/kc.sh "$@"

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
:root {
--pf-global--primary-color--100: #e2baff;
--pf-global--primary-color--200: #e2baff;
--pf-global--primary-color--dark-100: #e2baff;
--pf-global--Color--light-100: #20434f;
}
@font-face {
font-family: OpenCloud;
src: url('../fonts/OpenCloud500-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: OpenCloud;
src: url('../fonts/OpenCloud750-Bold.woff2') format('woff2');
font-weight: bold;
font-style: normal;
}
body {
font-family: "OpenCloud", "Open Sans", Helvetica, Arial, sans-serif;
background: url(../img/background.png) no-repeat center !important;
background-size: cover !important;
}
.kc-logo-text {
background-image: url(../img/logo.svg) !important;
background-size: contain;
width: 400px;
margin: 0 !important;
}
#kc-header-wrapper{
display: flex;
justify-content: center;
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,14 @@
<svg width="170" height="35" viewBox="0 0 170 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.1928 23.7428C42.5766 22.2684 41.0547 19.5569 41.0547 16.1555C41.0547 14.4433 41.4115 12.921 42.1485 11.589C43.5753 8.92532 46.2869 7.35547 49.6879 7.35547C51.377 7.35547 52.8514 7.73604 54.1593 8.47339C56.7523 9.97188 58.2508 12.7307 58.2508 16.1555C58.2508 17.8443 57.894 19.3663 57.1801 20.6748C55.753 23.315 53.042 24.8605 49.6879 24.8605C47.9992 24.8605 46.501 24.504 45.1928 23.7428ZM49.6641 22.1254C52.9942 22.1254 55.1824 19.7947 55.1824 16.1555C55.1824 12.4215 52.8755 10.0667 49.6641 10.0667C46.4534 10.0667 44.1224 12.4215 44.1224 16.1555C44.1224 19.771 46.3345 22.1254 49.6641 22.1254Z" fill="#E2BAFF"/>
<path d="M62.0912 11.9934L63.6608 14.7522C65.0879 12.4929 66.967 11.9934 68.4176 11.9934C69.5828 11.9934 70.5821 12.255 71.4618 12.8259C72.3657 13.4202 73.0558 14.1814 73.5077 15.1328C74.0069 16.1318 74.2448 17.2259 74.2448 18.4387C74.2448 19.6518 74.0069 20.7697 73.5077 21.7687C73.0558 22.7201 72.3419 23.4813 71.4381 24.0283C70.5821 24.5989 69.5828 24.8605 68.4176 24.8605C67.6327 24.8605 66.8477 24.6943 66.0869 24.3613C65.3971 24.0283 64.7787 23.5526 64.2792 22.958V28.9992H61.3774V16.5599L59.5938 13.4443L62.0912 11.9934ZM64.2792 18.4387C64.2792 19.1764 64.4219 19.8658 64.7073 20.4605C65.0641 21.1027 65.4685 21.5546 65.9442 21.84C66.4913 22.1493 67.0856 22.292 67.7754 22.292C68.489 22.292 69.0836 22.1493 69.6069 21.84C70.1302 21.5308 70.5583 21.0789 70.8196 20.4605C71.1289 19.8658 71.2478 19.1764 71.2478 18.4387C71.2478 17.2497 70.9386 16.2983 70.3204 15.6323C69.6782 14.9187 68.8457 14.562 67.7516 14.562C66.7291 14.562 65.8728 14.9187 65.2306 15.6323C64.6122 16.2983 64.2792 17.2497 64.2792 18.4387Z" fill="#E2BAFF"/>
<path d="M76.5405 15.133C77.0162 14.1812 77.7297 13.3966 78.7049 12.7782C79.7039 12.2311 80.7981 11.9695 82.0587 11.9695C83.319 11.9695 84.3656 12.2311 85.1981 12.7306C86.1017 13.3015 86.768 14.0626 87.2437 15.0137C87.6715 15.9892 87.9094 17.1544 87.9094 18.4386L87.8621 19.1286H78.6098C78.7049 20.1987 79.0855 21.0077 79.7515 21.6261C80.4175 22.1491 81.2976 22.387 82.3438 22.387C83.1528 22.387 83.8661 22.2446 84.5318 21.9826C85.1981 21.721 85.7925 21.388 86.2685 20.9836C86.3398 21.0553 87.505 23.1005 87.505 23.1005C86.768 23.6476 85.9352 24.0758 85.0316 24.4084C84.1991 24.7177 83.2239 24.8604 82.1535 24.8604C80.8694 24.8604 79.7277 24.599 78.8001 24.052C77.8962 23.5287 77.1589 22.7675 76.5642 21.7689C76.0409 20.7696 75.7793 19.6516 75.7793 18.4386C75.7793 17.2496 76.0172 16.1317 76.5405 15.133ZM78.6811 17.1544H85.0316C84.9365 16.203 84.6273 15.5373 84.104 15.1089C83.5331 14.6097 82.8671 14.348 82.0587 14.348C81.2259 14.348 80.5126 14.6097 79.8704 15.0616C79.2996 15.5135 78.919 16.203 78.6811 17.1544Z" fill="#E2BAFF"/>
<path d="M102.054 17.0594V24.4799H99.1283V17.3211C99.1283 16.3459 98.9142 15.6561 98.4623 15.2042C98.0342 14.6809 97.3919 14.443 96.5121 14.443C95.9411 14.443 95.4656 14.5857 94.9899 14.895C94.538 15.1328 94.1812 15.5372 93.9433 16.0367C93.7293 16.5124 93.5866 17.0832 93.5866 17.773V24.4799H90.6371V16.5599L88.8535 13.4202L91.3745 11.9934L92.8492 14.5144C94.3477 12.2785 95.9648 11.9934 97.059 11.9934C98.2958 11.9934 99.0569 12.2072 99.5564 12.445C100.056 12.6832 100.508 12.9921 100.841 13.3727C101.649 14.2527 102.054 15.4896 102.054 17.0594Z" fill="#E2BAFF"/>
<path d="M117.538 23.838C117.015 24.0999 116.325 24.3613 115.469 24.5991C114.684 24.7656 113.851 24.8608 112.971 24.8608C111.259 24.8608 109.737 24.4802 108.429 23.7669C107.144 23.0531 106.145 22.0303 105.408 20.6748C104.694 19.3663 104.338 17.8443 104.338 16.1083C104.338 14.4195 104.694 12.8972 105.384 11.5414C106.074 10.2097 107.144 9.16318 108.405 8.47339C109.713 7.71225 111.283 7.35547 113.138 7.35547C114.066 7.35547 114.874 7.42683 115.564 7.61711C116.373 7.80739 117.038 8.04525 117.514 8.33068C118.014 8.56854 118.608 8.90153 119.298 9.35346L117.8 11.8982C117.062 11.3987 116.349 11.0185 115.659 10.733C114.85 10.4238 113.994 10.2573 113.066 10.2573C111.83 10.2573 110.807 10.4952 109.999 10.9944C109.118 11.5414 108.476 12.2312 108.048 13.0875C107.62 14.0151 107.382 15.0379 107.382 16.1555C107.382 17.2738 107.62 18.2725 108.048 19.1525C108.476 20.0088 109.118 20.6986 109.999 21.1981C110.831 21.6735 111.854 21.9117 113.043 21.9117C113.733 21.9117 114.351 21.84 114.922 21.6973C115.54 21.5308 116.064 21.3408 116.491 21.103C117.086 20.7937 117.538 20.5083 117.871 20.2467L119.346 22.8152C118.632 23.2434 118.037 23.6001 117.538 23.838Z" fill="#E2BAFF"/>
<rect x="121.127" y="7.23633" width="2.90154" height="17.2437" fill="#E2BAFF"/>
<path d="M126.195 18.4387C126.195 14.6809 128.859 11.9934 132.688 11.9934C136.494 11.9934 139.134 14.6571 139.134 18.4152C139.134 22.2206 136.494 24.8605 132.664 24.8605C129.002 24.8605 126.195 22.3633 126.195 18.4387ZM132.664 22.3633C134.71 22.3633 136.113 20.9124 136.113 18.4152C136.113 15.9891 134.71 14.4671 132.664 14.4671C130.548 14.4671 129.192 16.0604 129.192 18.4152C129.192 20.8411 130.572 22.3633 132.664 22.3633Z" fill="#E2BAFF"/>
<path d="M145.86 24.8606C144.456 24.8606 143.339 24.4324 142.482 23.5048C141.674 22.5774 141.27 21.3168 141.27 19.747V12.374H144.171V19.5329C144.171 20.4605 144.385 21.1741 144.837 21.6736C145.265 22.1493 145.931 22.3634 146.811 22.3634C147.382 22.3634 147.881 22.2444 148.334 21.9828C148.785 21.7212 149.118 21.3168 149.356 20.8173C149.618 20.294 149.737 19.6994 149.737 19.081V12.374H152.662V20.3654L154.422 23.4337L151.925 24.8606L150.522 22.4585C150.522 22.4585 149.974 23.1721 149.237 23.7905C148.334 24.5517 147.382 24.8606 145.86 24.8606Z" fill="#E2BAFF"/>
<path d="M156.104 15.1328C156.651 14.0624 157.293 13.2775 158.173 12.8259C159.053 12.2788 160.076 11.9696 161.17 11.9696C162.098 11.9696 162.859 12.1599 163.525 12.4929C164.191 12.8259 164.809 13.254 165.309 13.8959V7.23657H168.21V20.3415L169.947 23.4099L167.449 24.837L165.927 22.1968C165.927 22.1968 164.88 23.6712 163.525 24.3375C162.954 24.623 162.05 24.837 161.194 24.837C160.076 24.837 159.053 24.5751 158.173 24.0045C157.317 23.5526 156.651 22.7677 156.104 21.7687C155.604 20.7697 155.391 19.6993 155.391 18.4387C155.391 17.1546 155.604 16.0604 156.104 15.1328ZM158.363 18.4387C158.363 19.2474 158.506 19.8896 158.792 20.437C159.125 21.1027 159.553 21.5073 160.004 21.7925C160.528 22.1017 161.098 22.2682 161.812 22.2682C162.526 22.2682 163.144 22.1017 163.644 21.7925C164.143 21.5073 164.571 21.0789 164.88 20.437C165.166 19.8896 165.309 19.2474 165.309 18.4387C165.309 17.2497 164.999 16.2507 164.333 15.6323C163.763 14.9187 162.883 14.5385 161.836 14.5385C160.766 14.5385 159.933 14.9187 159.339 15.6323C158.673 16.2507 158.363 17.2497 158.363 18.4387Z" fill="#E2BAFF"/>
<path d="M13.4814 25.5141L14.9505 24.6659V18.5276L20.234 15.4772V13.785L18.7649 12.9368L13.4462 16.0076L8.20127 12.9794L6.73242 13.8273V15.5198L12.0159 18.5703V24.668L13.4814 25.5141Z" fill="#E2BAFF"/>
<path d="M26.9649 7.78377L13.4828 0L0 7.78408V11.1725L13.4824 3.38806L26.9649 11.1721V7.78377Z" fill="#E2BAFF"/>
<path d="M26.9646 23.8279L13.4821 31.612L0 23.8279V27.2163L13.4821 35L26.9646 27.2163V23.8279Z" fill="#E2BAFF"/>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,19 @@
document.addEventListener("DOMContentLoaded", function () {
const setLogoUrl = (url) => {
const logoTextSelector = document.querySelector(".kc-logo-text");
if (!logoTextSelector) {
return
}
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
const parent = logoTextSelector.parentNode;
parent.insertBefore(link, logoTextSelector);
link.appendChild(logoTextSelector);
}
setLogoUrl('https://opencloud.eu')
});

View File

@@ -0,0 +1,5 @@
parent=keycloak
import=common/keycloak
styles=css/login.css css/theme.css
scripts=js/script.js

View File

@@ -0,0 +1,9 @@
#!/bin/bash
echo "Running custom LDAP entrypoint script..."
if [ ! -f /opt/bitnami/openldap/share/openldap.key ]
then
openssl req -x509 -newkey rsa:4096 -keyout /opt/bitnami/openldap/share/openldap.key -out /opt/bitnami/openldap/share/openldap.crt -sha256 -days 365 -batch -nodes
fi
# run original docker-entrypoint
/opt/bitnami/scripts/openldap/entrypoint.sh "$@"

View File

@@ -0,0 +1,20 @@
dn: dc=opencloud,dc=eu
objectClass: organization
objectClass: dcObject
dc: opencloud
o: openCloud
dn: ou=users,dc=opencloud,dc=eu
objectClass: organizationalUnit
ou: users
dn: cn=admin,dc=opencloud,dc=eu
objectClass: inetOrgPerson
objectClass: person
cn: admin
sn: admin
uid: ldapadmin
dn: ou=tenants,dc=opencloud,dc=eu
objectClass: organizationalUnit
ou: tenants

View File

@@ -0,0 +1,20 @@
dn: uid=admin,ou=users,dc=opencloud,dc=eu
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: admin
givenName: Admin
sn: Admin
cn: admin
displayName: Admin
description: An admin for this OpenCloud instance.
mail: admin@example.org
userPassword:: e1NTSEF9UWhmaFB3dERydTUydURoWFFObDRMbzVIckI3TkI5Nmo==
dn: cn=administrators,ou=groups,dc=opencloud,dc=eu
objectClass: groupOfNames
objectClass: top
cn: administrators
description: OpenCloud Administrators
member: uid=admin,ou=users,dc=opencloud,dc=eu

View File

@@ -0,0 +1,44 @@
directives:
child-src:
- '''self'''
connect-src:
- '''self'''
- 'blob:'
- 'https://${COMPANION_DOMAIN|companion.opencloud.test}/'
- 'wss://${COMPANION_DOMAIN|companion.opencloud.test}/'
- 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/'
- 'https://${IDP_DOMAIN|keycloak.opencloud.test}/'
default-src:
- '''none'''
font-src:
- '''self'''
frame-ancestors:
- '''self'''
frame-src:
- '''self'''
- 'blob:'
- 'https://embed.diagrams.net/'
# In contrary to bash and docker the default is given after the | character
- 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}/'
# This is needed for the external-sites web extension when embedding sites
- 'https://docs.opencloud.eu'
img-src:
- '''self'''
- 'data:'
- 'blob:'
- 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/'
# In contrary to bash and docker the default is given after the | character
- 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}/'
manifest-src:
- '''self'''
media-src:
- '''self'''
object-src:
- '''self'''
- 'blob:'
script-src:
- '''self'''
- '''unsafe-inline'''
style-src:
- '''self'''
- '''unsafe-inline'''

View File

@@ -0,0 +1,40 @@
# This adds four additional routes to the proxy. Forwarding
# request on '/carddav/', '/caldav/' and the respective '/.well-knwown'
# endpoints to the radicale container and setting the required headers.
additional_policies:
- name: default
routes:
- endpoint: /caldav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
- endpoint: /.well-known/caldav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
- endpoint: /carddav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
- endpoint: /.well-known/carddav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
# To enable the radicale web UI add this rule.
# "unprotected" is True because the Web UI itself ask for
# the password.
# Also set "type" to "internal" in the config/radicale/config
# - endpoint: /caldav/.web/
# backend: http://radicale:5232/
# unprotected: true
# skip_x_access_token: true
# additional_headers:
# - X-Script-Name: /caldav

View File

@@ -0,0 +1,72 @@
set -e
printenv
# Function to add arguments to the command
add_arg() {
TRAEFIK_CMD="$TRAEFIK_CMD $1"
}
# Initialize the base command
TRAEFIK_CMD="traefik"
# Base Traefik arguments (from your existing configuration)
add_arg "--log.level=${TRAEFIK_LOG_LEVEL:-ERROR}"
# enable dashboard
add_arg "--api.dashboard=true"
# define entrypoints
add_arg "--entryPoints.http.address=:80"
add_arg "--entryPoints.http.http.redirections.entryPoint.to=https"
add_arg "--entryPoints.http.http.redirections.entryPoint.scheme=https"
add_arg "--entryPoints.https.address=:443"
# change default timeouts for long-running requests
# this is needed for webdav clients that do not support the TUS protocol
add_arg "--entryPoints.https.transport.respondingTimeouts.readTimeout=12h"
add_arg "--entryPoints.https.transport.respondingTimeouts.writeTimeout=12h"
add_arg "--entryPoints.https.transport.respondingTimeouts.idleTimeout=3m"
# docker provider (get configuration from container labels)
add_arg "--providers.docker.endpoint=unix:///var/run/docker.sock"
add_arg "--providers.docker.exposedByDefault=false"
# access log
add_arg "--accessLog=${TRAEFIK_ACCESS_LOG:-false}"
add_arg "--accessLog.format=json"
add_arg "--accessLog.fields.headers.names.X-Request-Id=keep"
# Add Let's Encrypt configuration if enabled
if [ "${TRAEFIK_SERVICES_TLS_CONFIG}" = "tls.certresolver=letsencrypt" ]; then
echo "Configuring Traefik with Let's Encrypt..."
add_arg "--certificatesResolvers.letsencrypt.acme.email=${TRAEFIK_ACME_MAIL:-example@example.org}"
add_arg "--certificatesResolvers.letsencrypt.acme.storage=/certs/acme.json"
add_arg "--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http"
add_arg "--certificatesResolvers.letsencrypt.acme.caserver=${TRAEFIK_ACME_CASERVER:-https://acme-v02.api.letsencrypt.org/directory}"
fi
# Add local certificate configuration if enabled
if [ "${TRAEFIK_SERVICES_TLS_CONFIG}" = "tls=true" ]; then
echo "Configuring Traefik with local certificates..."
add_arg "--providers.file.directory=/etc/traefik/dynamic"
add_arg "--providers.file.watch=true"
fi
# Warning if neither certificate method is enabled
if [ "${TRAEFIK_SERVICES_TLS_CONFIG}" != "tls=true" ] && [ "${TRAEFIK_SERVICES_TLS_CONFIG}" != "tls.certresolver=letsencrypt" ]; then
echo "WARNING: Neither Let's Encrypt nor local certificates are enabled."
echo "HTTPS will not work properly without certificate configuration."
fi
# Add any custom arguments from environment variable
if [ -n "${TRAEFIK_CUSTOM_ARGS}" ]; then
echo "Adding custom Traefik arguments: ${TRAEFIK_CUSTOM_ARGS}"
TRAEFIK_CMD="$TRAEFIK_CMD $TRAEFIK_CUSTOM_ARGS"
fi
# Add any additional arguments passed to the script
for arg in "$@"; do
add_arg "$arg"
done
# Print the final command for debugging
echo "Starting Traefik with command:"
echo "$TRAEFIK_CMD"
# Execute Traefik
exec $TRAEFIK_CMD

View File

@@ -0,0 +1,119 @@
---
services:
# OpenCloud instance configured for multi-tenancy using keycloak as identity provider
# The graph service is setup to consume users via the CS3 API.
opencloud:
image: ${OC_DOCKER_IMAGE:-opencloudeu/opencloud-rolling}:${OC_DOCKER_TAG:-latest}
# changelog: https://github.com/opencloud-eu/opencloud/tree/main/changelog
# release notes: https://docs.opencloud.eu/opencloud_release_notes.html
networks:
opencloud-net:
entrypoint:
- /bin/sh
# run opencloud init to initialize a configuration file with random secrets
# it will fail on subsequent runs, because the config file already exists
# therefore we ignore the error and then start the opencloud server
command: ["-c", "opencloud init || true; opencloud server"]
environment:
OC_MULTI_TENANT_ENABLED: "true"
# enable services that are not started automatically
OC_URL: https://${OC_DOMAIN:-cloud.opencloud.test}
OC_LOG_LEVEL: ${LOG_LEVEL:-info}
OC_LOG_COLOR: "${LOG_PRETTY:-false}"
OC_LOG_PRETTY: "${LOG_PRETTY:-false}"
OC_EXCLUDE_RUN_SERVICES: idm,idp
PROXY_ROLE_ASSIGNMENT_DRIVER: "oidc"
OC_OIDC_ISSUER: https://${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}/realms/openCloud
PROXY_OIDC_REWRITE_WELLKNOWN: "true"
WEB_OIDC_CLIENT_ID: ${OC_OIDC_CLIENT_ID:-web}
PROXY_USER_OIDC_CLAIM: "uuid"
PROXY_USER_CS3_CLAIM: "userid"
WEB_OPTION_ACCOUNT_EDIT_LINK_HREF: "https://${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}/realms/openCloud/account"
# admin and demo accounts must be created in Keycloak
OC_ADMIN_USER_ID: ""
SETTINGS_SETUP_DEFAULT_ASSIGNMENTS: "false"
GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false"
GRAPH_USERNAME_MATCH: "none"
GROUPS_DRIVER: "null"
# This is needed to set the correct CSP rules for OpenCloud
IDP_DOMAIN: ${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}
# do not use SSL between the reverse proxy and OpenCloud
PROXY_TLS: "false"
# INSECURE: needed if OpenCloud / reverse proxy is using self generated certificates
OC_INSECURE: "${INSECURE:-false}"
# basic auth (not recommended, but needed for eg. WebDav clients that do not support OpenID Connect)
PROXY_ENABLE_BASIC_AUTH: "false"
GRAPH_IDENTITY_BACKEND: "cs3"
PROXY_CSP_CONFIG_FILE_LOCATION: /etc/opencloud/csp.yaml
OC_LDAP_URI: ldaps://ldap-server:1636
OC_LDAP_INSECURE: "true"
OC_LDAP_BIND_DN: "cn=admin,dc=opencloud,dc=eu"
OC_LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-admin}
OC_LDAP_USER_BASE_DN: "ou=users,dc=opencloud,dc=eu"
OC_LDAP_USER_SCHEMA_TENANT_ID: "openCloudMemberOfSchool"
PROXY_LOG_LEVEL: "debug"
volumes:
- ./config/opencloud/csp.yaml:/etc/opencloud/csp.yaml
# 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
logging:
driver: ${LOG_DRIVER:-local}
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.opencloud.entrypoints=https"
- "traefik.http.routers.opencloud.rule=Host(`${OC_DOMAIN:-cloud.opencloud.test}`)"
- "traefik.http.routers.opencloud.service=opencloud"
- "traefik.http.services.opencloud.loadbalancer.server.port=9200"
- "traefik.http.routers.opencloud.${TRAEFIK_SERVICES_TLS_CONFIG}"
# Stand-alone instance of the 'graph' service to serve the provisioning API
provsioning:
image: ${OC_DOCKER_IMAGE:-opencloudeu/opencloud-rolling}:${OC_DOCKER_TAG:-latest}
networks:
opencloud-net:
entrypoint:
- /bin/sh
# run opencloud init to initialize a configuration file with random secrets
# it will fail on subsequent runs, because the config file already exists
# therefore we ignore the error and then start the opencloud server
command: ["-c", "opencloud init || true; opencloud graph server"]
environment:
OC_LOG_LEVEL: "debug"
OC_LOG_COLOR: "${LOG_PRETTY:-false}"
OC_LOG_PRETTY: "${LOG_PRETTY:-false}"
# This just runs the standalone graph service we don't need access to the registry
MICRO_REGISTRY: "memory"
# INSECURE: needed if OpenCloud / reverse proxy is using self generated certificates
OC_INSECURE: "${INSECURE:-false}"
GRAPH_HTTP_ADDR: "0.0.0.0:9120"
GRAPH_HTTP_API_TOKEN: "${PROVISIONING_API_TOKEN:-changeme}"
# disable listening for events
GRAPH_EVENTS_ENDPOINT: ""
GRAPH_STORE_NODES: ""
GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false"
GRAPH_USERNAME_MATCH: "none"
GRAPH_LDAP_EDUCATION_RESOURCES_ENABLED: "true"
GRAPH_LDAP_SCHOOL_BASE_DN: "ou=tenants,dc=opencloud,dc=eu"
OC_LDAP_URI: ldaps://ldap-server:1636
OC_LDAP_INSECURE: "true"
OC_LDAP_BIND_DN: "cn=admin,dc=opencloud,dc=eu"
OC_LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-admin}
OC_LDAP_USER_BASE_DN: "ou=users,dc=opencloud,dc=eu"
OC_LDAP_USER_FILTER: "(objectclass=inetOrgPerson)"
volumes:
# configure the .env file to use own paths instead of docker internal volumes
- ${PROVISIONING_CONFIG_DIR:-provisioning-config}:/etc/opencloud
logging:
driver: ${LOG_DRIVER:-local}
restart: always
ports:
- "9120:9120"
volumes:
opencloud-config:
opencloud-data:
provisioning-config:
networks:
opencloud-net:

View File

@@ -0,0 +1,140 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"github.com/Nerzal/gocloak/v13"
"github.com/go-resty/resty/v2"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
const (
provisioningAPIURL = "http://localhost:9120/graph"
provisioningAuthToken = "changeme"
)
type tenantWithUsers struct {
tenant libregraph.EducationSchool
users []libregraph.EducationUser
}
var demoTenants = []tenantWithUsers{
{
tenant: libregraph.EducationSchool{
DisplayName: libregraph.PtrString("Famous Coders"),
},
users: []libregraph.EducationUser{
{
DisplayName: libregraph.PtrString("Dennis Ritchie"),
OnPremisesSamAccountName: libregraph.PtrString("dennis"),
Mail: libregraph.PtrString("dennis@example.org"),
},
{
DisplayName: libregraph.PtrString("Grace Hopper"),
OnPremisesSamAccountName: libregraph.PtrString("grace"),
Mail: libregraph.PtrString("grace@example.org"),
},
},
},
{
tenant: libregraph.EducationSchool{
DisplayName: libregraph.PtrString("Scientists"),
},
users: []libregraph.EducationUser{
{
DisplayName: libregraph.PtrString("Albert Einstein"),
OnPremisesSamAccountName: libregraph.PtrString("einstein"),
Mail: libregraph.PtrString("einstein@example.org"),
},
{
DisplayName: libregraph.PtrString("Marie Curie"),
OnPremisesSamAccountName: libregraph.PtrString("marie"),
Mail: libregraph.PtrString("marie@example.org"),
},
},
},
}
func main() {
lgconf := libregraph.NewConfiguration()
lgconf.Servers = libregraph.ServerConfigurations{
{
URL: provisioningAPIURL,
},
}
lgconf.DefaultHeader = map[string]string{"Authorization": "Bearer " + provisioningAuthToken}
lgclient := libregraph.NewAPIClient(lgconf)
for _, tenant := range demoTenants {
tenantid, err := createTenant(lgclient, tenant.tenant)
if err != nil {
fmt.Printf("Failed to create tenant: %s\n", err)
continue
}
for _, user := range tenant.users {
userid1, err := createUser(lgclient, user)
if err != nil {
fmt.Printf("Failed to create user: %s\n", err)
continue
}
_, err = lgclient.EducationSchoolApi.AddUserToSchool(context.TODO(), tenantid).EducationUserReference(libregraph.EducationUserReference{
OdataId: libregraph.PtrString(fmt.Sprintf("%s/education/users/%s", provisioningAPIURL, userid1)),
}).Execute()
if err != nil {
fmt.Printf("Failed to add user to tenant: %s\n", err)
continue
}
}
}
resetAllUserPasswords()
}
func createUser(client *libregraph.APIClient, user libregraph.EducationUser) (string, error) {
newUser, _, err := client.EducationUserApi.CreateEducationUser(context.TODO()).EducationUser(user).Execute()
if err != nil {
fmt.Printf("Failed to create user: %s\n", err)
return "", err
}
fmt.Printf("Created user: %s with id %s\n", newUser.GetDisplayName(), newUser.GetId())
return newUser.GetId(), nil
}
func createTenant(client *libregraph.APIClient, tenant libregraph.EducationSchool) (string, error) {
newTenant, _, err := client.EducationSchoolApi.CreateSchool(context.TODO()).EducationSchool(tenant).Execute()
if err != nil {
fmt.Printf("Failed to create tenant: %s\n", err)
return "", err
}
fmt.Printf("Created tenant: %s with id %s\n", newTenant.GetDisplayName(), newTenant.GetId())
return newTenant.GetId(), nil
}
func resetAllUserPasswords() {
tls := tls.Config{InsecureSkipVerify: true}
restyClient := resty.New().SetTLSClientConfig(&tls)
client := gocloak.NewClient("https://keycloak.opencloud.test")
client.SetRestyClient(restyClient)
ctx := context.Background()
token, err := client.LoginAdmin(ctx, "kcadmin", "admin", "master")
if err != nil {
fmt.Printf("Failed to login: %s\n", err)
panic("Something wrong with the credentials or url")
}
users, err := client.GetUsers(ctx, token.AccessToken, "openCloud", gocloak.GetUsersParams{})
if err != nil {
fmt.Printf("%s\n", err)
panic("Oh no!, failed to list users :(")
}
for _, user := range users {
fmt.Printf("Setting password for user: %s\n", *user.Username)
err = client.SetPassword(ctx, token.AccessToken, *user.ID, "openCloud", "demo", false)
if err != nil {
fmt.Printf("Failed to set password for user %s: %s\n", *user.Username, err)
}
}
}

View File

@@ -0,0 +1,55 @@
---
services:
opencloud:
environment:
postgres:
image: postgres:alpine
networks:
opencloud-net:
volumes:
- keycloak_postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: ${KC_DB_USERNAME:-keycloak}
POSTGRES_PASSWORD: ${KC_DB_PASSWORD:-keycloak}
logging:
driver: ${LOG_DRIVER:-local}
restart: always
keycloak:
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.entrypoints=https"
- "traefik.http.routers.keycloak.rule=Host(`${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}`)"
- "traefik.http.routers.keycloak.${TRAEFIK_SERVICES_TLS_CONFIG}"
- "traefik.http.routers.keycloak.service=keycloak"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
image: quay.io/keycloak/keycloak:26.4
networks:
opencloud-net:
command: [ "start", "--spi-connections-http-client-default-disable-trust-manager=${INSECURE:-false}", "--import-realm" ]
entrypoint: [ "/bin/sh", "/opt/keycloak/bin/docker-entrypoint-override.sh" ]
volumes:
- "./config/keycloak/docker-entrypoint-override.sh:/opt/keycloak/bin/docker-entrypoint-override.sh"
- "./config/keycloak/openCloud-realm.dist.json:/opt/keycloak/data/import-dist/openCloud-realm.json"
- "./config/keycloak/themes/opencloud:/opt/keycloak/themes/opencloud"
environment:
OC_DOMAIN: ${OC_DOMAIN:-cloud.opencloud.test}
KC_HOSTNAME: ${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}
KC_DB: postgres
KC_DB_URL: "jdbc:postgresql://postgres:5432/keycloak"
KC_DB_USERNAME: ${KC_DB_USERNAME:-keycloak}
KC_DB_PASSWORD: ${KC_DB_PASSWORD:-keycloak}
KC_FEATURES: impersonation
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: true
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-kcadmin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
depends_on:
- postgres
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
keycloak_postgres_data:

View File

@@ -0,0 +1,32 @@
---
services:
ldap-server:
image: bitnamilegacy/openldap:2.6
networks:
opencloud-net:
entrypoint: [ "/bin/sh", "/opt/bitnami/scripts/openldap/docker-entrypoint-override.sh", "/opt/bitnami/scripts/openldap/run.sh" ]
environment:
BITNAMI_DEBUG: true
LDAP_TLS_VERIFY_CLIENT: never
LDAP_ENABLE_TLS: "yes"
LDAP_TLS_CA_FILE: /opt/bitnami/openldap/share/openldap.crt
LDAP_TLS_CERT_FILE: /opt/bitnami/openldap/share/openldap.crt
LDAP_TLS_KEY_FILE: /opt/bitnami/openldap/share/openldap.key
LDAP_ROOT: "dc=opencloud,dc=eu"
LDAP_ADMIN_PASSWORD: ${LDAP_BIND_PASSWORD:-admin}
ports:
- "127.0.0.1:389:1389"
- "127.0.0.1:636:1636"
volumes:
# Only use the base ldif file to create the base structure
- ./config/ldap/ldif/10_base.ldif:/ldifs/10_base.ldif
# Use the custom schema from opencloud because we are in full control of the ldap server
- ../shared/config/ldap/schemas/10_opencloud_schema.ldif:/schemas/10_opencloud_schema.ldif
- ../shared/config/ldap/schemas/20_opencloud_education_schema.ldif:/schemas/20_opencloud_education_schema.ldif
- ./config/ldap/docker-entrypoint-override.sh:/opt/bitnami/scripts/openldap/docker-entrypoint-override.sh
- ${LDAP_CERTS_DIR:-ldap-certs}:/opt/bitnami/openldap/share
- ${LDAP_DATA_DIR:-ldap-data}:/bitnami/openldap
volumes:
ldap-certs:
ldap-data:

View File

@@ -0,0 +1,24 @@
---
# This file can be used to be added to the opencloud_full example
# to browse the LDAP server with a web interface.
# This is not a production ready setup.
services:
ldap-manager:
image: phpldapadmin/phpldapadmin:latest
networks:
opencloud-net:
environment:
LDAP_HOST: ldap-server
LDAP_PORT: 1389
LDAP_LOGIN_OBJECTCLASS: "inetOrgPerson"
APP_URL: "https://${LDAP_MANAGER_DOMAIN:-ldap.opencloud.test}"
labels:
- "traefik.enable=true"
- "traefik.http.routers.ldap-manager.entrypoints=https"
- "traefik.http.routers.ldap-manager.rule=Host(`${LDAP_MANAGER_DOMAIN:-ldap.opencloud.test}`)"
- "traefik.http.routers.ldap-manager.${TRAEFIK_SERVICES_TLS_CONFIG}"
- "traefik.http.routers.ldap-manager.service=ldap-manager"
- "traefik.http.services.ldap-manager.loadbalancer.server.port=8080"
logging:
driver: ${LOG_DRIVER:-local}
restart: always

View File

@@ -0,0 +1,45 @@
---
services:
opencloud:
labels:
- "traefik.enable=true"
- "traefik.http.routers.opencloud.entrypoints=https"
- "traefik.http.routers.opencloud.rule=Host(`${OC_DOMAIN:-cloud.opencloud.test}`)"
- "traefik.http.routers.opencloud.service=opencloud"
- "traefik.http.services.opencloud.loadbalancer.server.port=9200"
- "traefik.http.routers.opencloud.${TRAEFIK_SERVICES_TLS_CONFIG}"
traefik:
image: traefik:v3.3.1
# release notes: https://github.com/traefik/traefik/releases
networks:
opencloud-net:
aliases:
- ${OC_DOMAIN:-cloud.opencloud.test}
- ${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}
entrypoint: [ "/bin/sh", "/opt/traefik/bin/docker-entrypoint-override.sh"]
environment:
- "TRAEFIK_SERVICES_TLS_CONFIG=${TRAEFIK_SERVICES_TLS_CONFIG:-tls.certresolver=letsencrypt}"
- "TRAEFIK_ACME_MAIL=${TRAEFIK_ACME_MAIL:-example@example.org}"
- "TRAEFIK_ACME_CASERVER=${TRAEFIK_ACME_CASERVER:-https://acme-v02.api.letsencrypt.org/directory}"
- "TRAEFIK_LOG_LEVEL=${TRAEFIK_LOG_LEVEL:-ERROR}"
- "TRAEFIK_ACCESS_LOG=${TRAEFIK_ACCESS_LOG:-false}"
ports:
- "80:80"
- "443:443"
volumes:
- "${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock:ro"
- "./config/traefik/docker-entrypoint-override.sh:/opt/traefik/bin/docker-entrypoint-override.sh"
- "${TRAEFIK_CERTS_DIR:-./certs}:/certs"
- "./config/traefik/dynamic:/etc/traefik/dynamic"
labels:
- "traefik.enable=${TRAEFIK_DASHBOARD:-false}"
# defaults to admin:admin
- "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_BASIC_AUTH_USERS:-admin:$$apr1$$4vqie50r$$YQAmQdtmz5n9rEALhxJ4l.}"
- "traefik.http.routers.traefik.entrypoints=https"
- "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DOMAIN:-traefik.opencloud.test}`)"
- "traefik.http.routers.traefik.middlewares=traefik-auth"
- "traefik.http.routers.traefik.${TRAEFIK_SERVICES_TLS_CONFIG}"
- "traefik.http.routers.traefik.service=api@internal"
logging:
driver: ${LOG_DRIVER:-local}
restart: always

37
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/Nerzal/gocloak/v13 v13.9.0
github.com/bbalet/stopwords v1.0.0
github.com/beevik/etree v1.6.0
github.com/blevesearch/bleve/v2 v2.5.4
github.com/blevesearch/bleve/v2 v2.5.5
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.16.0
github.com/cs3org/go-cs3apis v0.0.0-20250908152307-4ca807afe54e
@@ -57,14 +57,14 @@ require (
github.com/nats-io/nats-server/v2 v2.12.1
github.com/nats-io/nats.go v1.47.0
github.com/oklog/run v1.2.0
github.com/olekukonko/tablewriter v1.1.0
github.com/olekukonko/tablewriter v1.1.1
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.27.2
github.com/onsi/gomega v1.38.2
github.com/open-policy-agent/opa v1.10.1
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
github.com/opencloud-eu/reva/v2 v2.39.3-0.20251113164418-9fd6b6864c10
github.com/opencloud-eu/reva/v2 v2.39.3-0.20251121093521-c51ed14c8397
github.com/opensearch-project/opensearch-go/v4 v4.5.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
@@ -101,18 +101,19 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/crypto v0.43.0
golang.org/x/crypto v0.44.0
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/image v0.32.0
golang.org/x/net v0.46.0
golang.org/x/oauth2 v0.33.0
golang.org/x/sync v0.18.0
golang.org/x/term v0.37.0
golang.org/x/text v0.30.0
golang.org/x/text v0.31.0
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
stash.kopano.io/kgol/rndm v1.1.2
)
@@ -139,13 +140,13 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/blevesearch/bleve_index_api v1.2.10 // indirect
github.com/blevesearch/bleve_index_api v1.2.11 // indirect
github.com/blevesearch/geo v0.2.4 // indirect
github.com/blevesearch/go-faiss v1.0.25 // indirect
github.com/blevesearch/go-faiss v1.0.26 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.12 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // 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
@@ -155,7 +156,7 @@ require (
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.2.6 // indirect
github.com/blevesearch/zapx/v16 v16.2.7 // 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
@@ -163,6 +164,9 @@ require (
github.com/ceph/go-ceph v0.36.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cevaris/ordered_map v0.0.0-20190319150403-3adeae072e73 // indirect
github.com/clipperhouse/displaywidth v0.3.1 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
@@ -238,7 +242,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.6 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/renameio/v2 v2.0.0 // indirect
github.com/google/renameio/v2 v2.0.1 // indirect
github.com/gookit/goutil v0.7.1 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/schema v1.4.1 // indirect
@@ -276,7 +280,7 @@ require (
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.16 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/maxymania/go-system v0.0.0-20170110133659-647cc364bf0b // indirect
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 // indirect
@@ -304,8 +308,9 @@ require (
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/ll v0.1.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
@@ -325,11 +330,13 @@ require (
github.com/prometheus/procfs v0.17.0 // indirect
github.com/prometheus/statsd_exporter v0.22.8 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russellhaering/goxmldsig v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/segmentio/kafka-go v0.4.49 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
@@ -385,7 +392,6 @@ require (
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
@@ -400,3 +406,6 @@ replace go-micro.dev/v4 => github.com/butonic/go-micro/v4 v4.11.1-0.202411151126
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
replace github.com/go-micro/plugins/v4/store/nats-js-kv => github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a
// to get the logger injection (https://github.com/pablodz/inotifywaitgo/pull/11)
replace github.com/pablodz/inotifywaitgo v0.0.9 => github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9

69
go.sum
View File

@@ -151,22 +151,22 @@ github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blevesearch/bleve/v2 v2.5.4 h1:1iur8e+PHsxtncV2xIVuqlQme/V8guEDO2uV6Wll3lQ=
github.com/blevesearch/bleve/v2 v2.5.4/go.mod h1:yB4PnV4N2q5rTEpB2ndG8N2ISexBQEFIYgwx4ztfvoo=
github.com/blevesearch/bleve_index_api v1.2.10 h1:FMFmZCmTX6PdoLLvwUnKF2RsmILFFwO3h0WPevXY9fE=
github.com/blevesearch/bleve_index_api v1.2.10/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/bleve/v2 v2.5.5 h1:lzC89QUCco+y1qBnJxGqm4AbtsdsnlUvq0kXok8n3C8=
github.com/blevesearch/bleve/v2 v2.5.5/go.mod h1:t5WoESS5TDteTdnjhhvpA1BpLYErOBX2IQViTMLK7wo=
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
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.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.3.12 h1:GGZc2qwbyRBwtckPPkHkLyXw64mmsLJxdturBI1cM+c=
github.com/blevesearch/scorch_segment_api/v2 v2.3.12/go.mod h1:JBRGAneqgLSI2+jCNjtwMqp2B7EBF3/VUzgDPIU33MM=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
@@ -185,8 +185,8 @@ github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.2.6 h1:OHuUl2GhM+FpBq9RwNsJ4k/QodqbMMHoQEgn/IHYpu8=
github.com/blevesearch/zapx/v16 v16.2.6/go.mod h1:cuAPB+YoIyRngNhno1S1GPr9SfMk+x/SgAHBLXSIq3k=
github.com/blevesearch/zapx/v16 v16.2.7 h1:xcgFRa7f/tQXOwApVq7JWgPYSlzyUMmkuYa54tMDuR0=
github.com/blevesearch/zapx/v16 v16.2.7/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
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=
@@ -223,6 +223,12 @@ 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.3.1 h1:k07iN9gD32177o1y4O1jQMzbLdCrsGJh+blirVYybsk=
github.com/clipperhouse/displaywidth v0.3.1/go.mod h1:tgLJKKyaDOCadywag3agw4snxS5kYEuYR6Y9+qWDDYM=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304=
@@ -581,8 +587,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/google/renameio/v2 v2.0.1 h1:HyOM6qd9gF9sf15AvhbptGHUnaLTpEI9akAFFU3VyW0=
github.com/google/renameio/v2 v2.0.1/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -821,8 +827,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
@@ -924,13 +930,15 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0=
github.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/olekukonko/tablewriter v1.1.1 h1:b3reP6GCfrHwmKkYwNRFh2rxidGHcT6cgxj/sHiDDx0=
github.com/olekukonko/tablewriter v1.1.1/go.mod h1:De/bIcTF+gpBDB3Alv3fEsZA+9unTsSzAg/ZGADCtn4=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@@ -949,10 +957,12 @@ github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-202505121527
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/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIftlX03Bzfbujhp9B54FbgER0VBDWJi/w8RBxJlzxU=
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76 h1:vD/EdfDUrv4omSFjrinT8Mvf+8D7f9g4vgQ2oiDrVUI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.39.3-0.20251113164418-9fd6b6864c10 h1:9b5O3lzYHmR+aDNo81UYMcDGfUARrHw5Suk4YmqNgJA=
github.com/opencloud-eu/reva/v2 v2.39.3-0.20251113164418-9fd6b6864c10/go.mod h1:YxP7b+8olAhgbQBUUnsRQokgf1RkwpEBLq614XXXXHA=
github.com/opencloud-eu/reva/v2 v2.39.3-0.20251121093521-c51ed14c8397 h1:69kNapq4vaOfe6+KNF7Q7BibUjluCnK8VuS2UXigkjU=
github.com/opencloud-eu/reva/v2 v2.39.3-0.20251121093521-c51ed14c8397/go.mod h1:iB6Z8rgsbVMYMvicUm00ZwkwJHQow38K/GUSJgAPgEo=
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=
@@ -969,8 +979,6 @@ github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CF
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw=
github.com/pablodz/inotifywaitgo v0.0.9/go.mod h1:hAfx2oN+WKg8miwUKPs52trySpPignlRBRxWcXVHku0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
@@ -1064,9 +1072,6 @@ github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8A
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/riandyrn/otelchi v0.12.2 h1:6QhGv0LVw/dwjtPd12mnNrl0oEQF4ZAlmHcnlTYbeAg=
github.com/riandyrn/otelchi v0.12.2/go.mod h1:weZZeUJURvtCcbWsdb7Y6F8KFZGedJlSrgUjq9VirV8=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -1086,6 +1091,12 @@ github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
@@ -1346,8 +1357,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
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=
@@ -1586,8 +1597,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
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=

View File

@@ -0,0 +1,20 @@
# App Provider
The `app-provider` service provides the CS3 App Provider API for OpenCloud. It is responsible for managing and serving applications that can open files based on their MIME types.
The service works in conjunction with the `app-registry` service, which maintains the registry of available applications and their supported MIME types. When a client requests to open a file with a specific application, the `app-provider` service handles the request and coordinates with the application to provide the appropriate interface.
## Integration
The `app-provider` service integrates with:
- `app-registry` - For discovering which applications are available for specific MIME types
- `frontend` - The frontend service forwards app provider requests (default endpoint `/app`) to this service
## Configuration
The service can be configured via environment variables. Key configuration options include:
- `APP_PROVIDER_EXTERNAL_ADDR` - External address where the gateway service can reach the app provider
## Scalability
The app-provider service can be scaled horizontally as it primarily acts as a coordinator between applications and the OpenCloud backend services.

View File

@@ -36,6 +36,7 @@ type Config struct {
SearchMinLength int `yaml:"search_min_length" env:"FRONTEND_SEARCH_MIN_LENGTH" desc:"Minimum number of characters to enter before a client should start a search for Share receivers. This setting can be used to customize the user experience if e.g too many results are displayed." introductionVersion:"1.0.0"`
Edition string `yaml:"edition" env:"OC_EDITION;FRONTEND_EDITION" desc:"Edition of OpenCloud. Used for branding purposes." introductionVersion:"1.0.0"`
DisableSSE bool `yaml:"disable_sse" env:"OC_DISABLE_SSE;FRONTEND_DISABLE_SSE" desc:"When set to true, clients are informed that the Server-Sent Events endpoint is not accessible." introductionVersion:"1.0.0"`
DisableRadicale bool `yaml:"disable_radicale" env:"FRONTEND_DISABLE_RADICALE" desc:"When set to true, clients are informed that the Radicale (CalDAV/CardDAV) is not accessible." introductionVersion:"4.0.0"`
DefaultLinkPermissions int `yaml:"default_link_permissions" env:"FRONTEND_DEFAULT_LINK_PERMISSIONS" desc:"Defines the default permissions a link is being created with. Possible values are 0 (= internal link, for instance members only) and 1 (= public link with viewer permissions). Defaults to 1." introductionVersion:"1.0.0"`
PublicURL string `yaml:"public_url" env:"OC_URL;FRONTEND_PUBLIC_URL" desc:"The public facing URL of the OpenCloud frontend." introductionVersion:"1.0.0"`

View File

@@ -218,6 +218,7 @@ func FrontendConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string
"check_for_updates": cfg.CheckForUpdates,
"support_url_signing": true,
"support_sse": !cfg.DisableSSE,
"support_radicale": !cfg.DisableRadicale,
},
"graph": map[string]interface{}{
"personal_data_export": true,

View File

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

29
services/groups/README.md Normal file
View File

@@ -0,0 +1,29 @@
# Groups
The `groups` service provides the CS3 Groups API for OpenCloud. It is responsible for managing group information and memberships within the OpenCloud instance.
This service implements the CS3 identity group provider interface, allowing other services to query and manage groups. It works as a backend provider for the `graph` service when using the CS3 backend mode.
## Backend Integration
The groups service can work with different storage backends:
- LDAP integration through the graph service
- Direct CS3 API implementation
When using the `graph` service with the CS3 backend (`GRAPH_IDENTITY_BACKEND=cs3`), the graph service queries group information through this service.
## API
The service provides CS3 gRPC APIs for:
- Listing groups
- Getting group information
- Finding groups by name or ID
- Managing group memberships
## Usage
The groups service is only used internally by other OpenCloud services and not being accessed directly by clients. The `frontend` and `ocs` services translate HTTP API requests into CS3 API calls to this service.
## Scalability
Since the groups service queries backend systems (like LDAP through the configured identity backend), it can be scaled horizontally without additional configuration when using stateless backends.

View File

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

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-11-07 00:02+0000\n"
"POT-Creation-Date: 2025-11-24 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"
@@ -84,7 +84,7 @@ msgstr "ScienceMesh: {InitiatorName} haluaa tehdä yhteistyötä kanssasi"
#. ShareExpired email template, Subject field (resolves directly)
#: pkg/email/templates.go:30
msgid "Share to '{ShareFolder}' expired at {ExpiredAt}"
msgstr ""
msgstr "Jako kansioon '{ShareFolder}' vanheni {ExpiredAt}"
#. MembershipExpired email template, resolves via {{ .MessageBody }}
#: pkg/email/templates.go:76

View File

@@ -24,27 +24,28 @@ type Config struct {
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
GrpcClient client.Client `yaml:"-"`
RoleQuotas map[string]uint64 `yaml:"role_quotas"`
Policies []Policy `yaml:"policies"`
AdditionalPolicies []Policy `yaml:"additional_policies"`
OIDC OIDC `yaml:"oidc"`
ServiceAccount ServiceAccount `yaml:"service_account"`
RoleAssignment RoleAssignment `yaml:"role_assignment"`
PolicySelector *PolicySelector `yaml:"policy_selector"`
PreSignedURL PreSignedURL `yaml:"pre_signed_url"`
AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here." introductionVersion:"1.0.0"`
UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that is used for resolving users with the account backend. The value of the claim must hold a per user unique, stable and non re-assignable identifier. The availability of claims depends on your Identity Provider. There are common claims available for most Identity providers like 'email' or 'preferred_username' but you can also add your own claim." introductionVersion:"1.0.0"`
UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'." introductionVersion:"1.0.0"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services." introductionVersion:"1.0.0" mask:"password"`
AutoprovisionAccounts bool `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provision users that do not yet exist in the users service on-demand upon first sign-in. To use this a write-enabled libregraph user backend needs to be setup an running." introductionVersion:"1.0.0"`
AutoProvisionClaims AutoProvisionClaims `yaml:"auto_provision_claims"`
EnableBasicAuth bool `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic authentication' (username/password)." introductionVersion:"1.0.0"`
InsecureBackends bool `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all HTTP backend connections." introductionVersion:"1.0.0"`
BackendHTTPSCACert string `yaml:"backend_https_cacert" env:"PROXY_HTTPS_CACERT" desc:"Path/File for the root CA certificate used to validate the servers TLS certificate for https enabled backend services." introductionVersion:"1.0.0"`
AuthMiddleware AuthMiddleware `yaml:"auth_middleware"`
PoliciesMiddleware PoliciesMiddleware `yaml:"policies_middleware"`
CSPConfigFileLocation string `yaml:"csp_config_file_location" env:"PROXY_CSP_CONFIG_FILE_LOCATION" desc:"The location of the CSP configuration file." introductionVersion:"1.0.0"`
Events Events `yaml:"events"`
RoleQuotas map[string]uint64 `yaml:"role_quotas"`
Policies []Policy `yaml:"policies"`
AdditionalPolicies []Policy `yaml:"additional_policies"`
OIDC OIDC `yaml:"oidc"`
ServiceAccount ServiceAccount `yaml:"service_account"`
RoleAssignment RoleAssignment `yaml:"role_assignment"`
PolicySelector *PolicySelector `yaml:"policy_selector"`
PreSignedURL PreSignedURL `yaml:"pre_signed_url"`
AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here." introductionVersion:"1.0.0"`
UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that is used for resolving users with the account backend. The value of the claim must hold a per user unique, stable and non re-assignable identifier. The availability of claims depends on your Identity Provider. There are common claims available for most Identity providers like 'email' or 'preferred_username' but you can also add your own claim." introductionVersion:"1.0.0"`
UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'." introductionVersion:"1.0.0"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services." introductionVersion:"1.0.0" mask:"password"`
AutoprovisionAccounts bool `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provision users that do not yet exist in the users service on-demand upon first sign-in. To use this a write-enabled libregraph user backend needs to be setup an running." introductionVersion:"1.0.0"`
AutoProvisionClaims AutoProvisionClaims `yaml:"auto_provision_claims"`
EnableBasicAuth bool `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic authentication' (username/password)." introductionVersion:"1.0.0"`
InsecureBackends bool `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all HTTP backend connections." introductionVersion:"1.0.0"`
BackendHTTPSCACert string `yaml:"backend_https_cacert" env:"PROXY_HTTPS_CACERT" desc:"Path/File for the root CA certificate used to validate the servers TLS certificate for https enabled backend services." introductionVersion:"1.0.0"`
AuthMiddleware AuthMiddleware `yaml:"auth_middleware"`
PoliciesMiddleware PoliciesMiddleware `yaml:"policies_middleware"`
CSPConfigFileLocation string `yaml:"csp_config_file_location" env:"PROXY_CSP_CONFIG_FILE_LOCATION" desc:"The location of the CSP configuration file." introductionVersion:"1.0.0"`
CSPConfigFileOverrideLocation string `yaml:"csp_config_file_override_location" env:"PROXY_CSP_CONFIG_FILE_OVERRIDE_LOCATION" desc:"The location of the CSP configuration file override." introductionVersion:"%%NEXT%%"`
Events Events `yaml:"events"`
Context context.Context `json:"-" yaml:"-"`
}

View File

@@ -5,6 +5,7 @@ directives:
- '''self'''
- 'blob:'
- 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/'
- 'https://update.opencloud.eu/'
default-src:
- '''none'''
font-src:

View File

@@ -92,9 +92,10 @@ func DefaultConfig() *config.Config {
DisplayName: "name",
Groups: "groups",
},
EnableBasicAuth: false,
InsecureBackends: false,
CSPConfigFileLocation: "",
EnableBasicAuth: false,
InsecureBackends: false,
CSPConfigFileLocation: "",
CSPConfigFileOverrideLocation: "",
Events: config.Events{
Endpoint: "127.0.0.1:9233",
Cluster: "opencloud-cluster",

View File

@@ -1,13 +1,11 @@
package middleware
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/jellydator/ttlcache/v3"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/router"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend"
@@ -126,14 +124,6 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
// keycloak automatically base64 encodes binary uuids like objectGUID, try to decode it
if decoded, decodeErr := base64.URLEncoding.DecodeString(value); decodeErr == nil {
if parsed, parseErr := uuid.FromBytes(decoded); parseErr == nil {
m.logger.Debug().Str("claim", m.userOIDCClaim).Str("value", value).Str("parsed", parsed.String()).Msg("Detected base64 encoded binary uuid")
value = parsed.String()
}
}
user, token, err = m.userProvider.GetUserByClaims(req.Context(), m.userCS3Claim, value)
if errors.Is(err, backend.ErrAccountNotFound) {

View File

@@ -21,10 +21,10 @@ import (
)
func TestTokenIsAddedWithMailClaim(t *testing.T) {
sut := newMockAccountResolver(t, &userv1beta1.User{
sut := newMockAccountResolver(&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, oidc.Email, "mail", false, "foo@example.com")
}, nil, oidc.Email, "mail", false)
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
@@ -39,10 +39,10 @@ func TestTokenIsAddedWithMailClaim(t *testing.T) {
}
func TestTokenIsAddedWithUsernameClaim(t *testing.T) {
sut := newMockAccountResolver(t, &userv1beta1.User{
sut := newMockAccountResolver(&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, oidc.PreferredUsername, "username", false, "foo")
}, nil, oidc.PreferredUsername, "username", false)
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
@@ -58,10 +58,10 @@ func TestTokenIsAddedWithUsernameClaim(t *testing.T) {
}
func TestTokenIsAddedWithDotUsernamePathClaim(t *testing.T) {
sut := newMockAccountResolver(t, &userv1beta1.User{
sut := newMockAccountResolver(&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, "li.un", "username", false, "foo")
}, nil, "li.un", "username", false)
// This is how lico adds the username to the access token
req, rw := mockRequest(map[string]interface{}{
@@ -80,10 +80,10 @@ func TestTokenIsAddedWithDotUsernamePathClaim(t *testing.T) {
}
func TestTokenIsAddedWithDotEscapedUsernameClaim(t *testing.T) {
sut := newMockAccountResolver(t, &userv1beta1.User{
sut := newMockAccountResolver(&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, "li\\.un", "username", false, "foo")
}, nil, "li\\.un", "username", false)
// This tests the . escaping of the readUserIDClaim
req, rw := mockRequest(map[string]interface{}{
@@ -100,10 +100,10 @@ func TestTokenIsAddedWithDotEscapedUsernameClaim(t *testing.T) {
}
func TestTokenIsAddedWithDottedUsernameClaimFallback(t *testing.T) {
sut := newMockAccountResolver(t, &userv1beta1.User{
sut := newMockAccountResolver(&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, "li.un", "username", false, "foo")
}, nil, "li.un", "username", false)
// This tests the . escaping fallback of the readUserIDClaim
req, rw := mockRequest(map[string]interface{}{
@@ -120,7 +120,7 @@ func TestTokenIsAddedWithDottedUsernameClaimFallback(t *testing.T) {
}
func TestNSkipOnNoClaims(t *testing.T) {
sut := newMockAccountResolver(t, nil, backend.ErrAccountDisabled, oidc.Email, "mail", false, "")
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.Email, "mail", false)
req, rw := mockRequest(nil)
sut.ServeHTTP(rw, req)
@@ -131,7 +131,7 @@ func TestNSkipOnNoClaims(t *testing.T) {
}
func TestUnauthorizedOnUserNotFound(t *testing.T) {
sut := newMockAccountResolver(t, nil, backend.ErrAccountNotFound, oidc.PreferredUsername, "username", false, "foo")
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.PreferredUsername, "username", false)
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
@@ -145,7 +145,7 @@ func TestUnauthorizedOnUserNotFound(t *testing.T) {
}
func TestUnauthorizedOnUserDisabled(t *testing.T) {
sut := newMockAccountResolver(t, nil, backend.ErrAccountDisabled, oidc.PreferredUsername, "username", false, "foo")
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.PreferredUsername, "username", false)
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
@@ -159,7 +159,7 @@ func TestUnauthorizedOnUserDisabled(t *testing.T) {
}
func TestInternalServerErrorOnMissingMailAndUsername(t *testing.T) {
sut := newMockAccountResolver(t, nil, backend.ErrAccountNotFound, oidc.Email, "mail", false, "")
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.Email, "mail", false)
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
})
@@ -173,11 +173,11 @@ func TestInternalServerErrorOnMissingMailAndUsername(t *testing.T) {
func TestUnauthorizedOnMissingTenantId(t *testing.T) {
sut := newMockAccountResolver(
t, &userv1beta1.User{
&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Username: "foo",
},
nil, oidc.PreferredUsername, "username", true, "foo")
nil, oidc.PreferredUsername, "username", true)
req, rw := mockRequest(map[string]any{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
@@ -192,7 +192,7 @@ func TestUnauthorizedOnMissingTenantId(t *testing.T) {
func TestTokenIsAddedWhenUserHasTenantId(t *testing.T) {
sut := newMockAccountResolver(
t, &userv1beta1.User{
&userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "https://idx.example.com",
OpaqueId: "123",
@@ -200,7 +200,7 @@ func TestTokenIsAddedWhenUserHasTenantId(t *testing.T) {
},
Username: "foo",
},
nil, oidc.PreferredUsername, "username", true, "foo")
nil, oidc.PreferredUsername, "username", true)
req, rw := mockRequest(map[string]any{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
@@ -213,46 +213,7 @@ func TestTokenIsAddedWhenUserHasTenantId(t *testing.T) {
assert.Contains(t, token, "eyJ")
}
func TestTokenIsAddedWithBinaryBase64UserIDClaim(t *testing.T) {
sut := newMockAccountResolver(t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "b4963c7c-72b3-44b4-af98-eee1429dd8c2"},
Mail: "foo@example.com",
}, nil, "uuid", "username", false, "b4963c7c-72b3-44b4-af98-eee1429dd8c2")
// This tests the base64 decoding of binary user id claims
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
"uuid": "tJY8fHKzRLSvmO7hQp3Ywg==", // base64 encoded bytes value of b4963c7c-72b3-44b4-af98-eee1429dd8c2
})
sut.ServeHTTP(rw, req)
token := req.Header.Get(revactx.TokenHeader)
assert.NotEmpty(t, token)
assert.Contains(t, token, "eyJ")
}
func TestTokenIsAddedWithInvalidBinaryBase64UserIDClaim(t *testing.T) {
sut := newMockAccountResolver(t, &userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "b4963c7c-72b3-44b4-af98-eee1429dd8c2"},
Mail: "foo@example.com",
}, nil, "uuid", "username", false, "aGVsbG8gd29ybGQ=")
// This tests the base64 decoding of binary user id claims
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
"uuid": "aGVsbG8gd29ybGQ=", // base64 encoded bytes value of invalid uuid
})
sut.ServeHTTP(rw, req)
token := req.Header.Get(revactx.TokenHeader)
assert.NotEmpty(t, token)
assert.Contains(t, token, "eyJ")
}
func newMockAccountResolver(t *testing.T, userBackendResult *userv1beta1.User, userBackendErr error, oidcclaim, cs3claim string, multiTenant bool, expectedClaim string) http.Handler {
func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr error, oidcclaim, cs3claim string, multiTenant bool) http.Handler {
tokenManager, _ := jwt.New(map[string]interface{}{
"secret": "change-me",
"expires": int64(60),
@@ -265,13 +226,7 @@ func newMockAccountResolver(t *testing.T, userBackendResult *userv1beta1.User, u
}
ub := mocks.UserBackend{}
ub.On("GetUserByClaims", mock.Anything, mock.Anything,
mock.MatchedBy(
func(claim string) bool {
assert.Equal(t, expectedClaim, claim)
return true
})).
Return(userBackendResult, token, userBackendErr)
ub.On("GetUserByClaims", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(userBackendResult, token, userBackendErr)
ub.On("GetUserRoles", mock.Anything, mock.Anything).Return(userBackendResult, nil)
ra := userRoleMocks.UserRoleAssigner{}

View File

@@ -3,30 +3,48 @@ package middleware
import (
"net/http"
"os"
"reflect"
gofig "github.com/gookit/config/v2"
"github.com/gookit/config/v2/yaml"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/config"
"github.com/unrolled/secure"
"github.com/unrolled/secure/cspbuilder"
yamlv3 "gopkg.in/yaml.v3"
)
// LoadCSPConfig loads CSP header configuration from a yaml file.
func LoadCSPConfig(proxyCfg *config.Config) (*config.CSP, error) {
yamlContent, err := loadCSPYaml(proxyCfg)
yamlContent, customYamlContent, err := loadCSPYaml(proxyCfg)
if err != nil {
return nil, err
}
return loadCSPConfig(yamlContent)
return loadCSPConfig(yamlContent, customYamlContent)
}
// LoadCSPConfig loads CSP header configuration from a yaml file.
func loadCSPConfig(yamlContent []byte) (*config.CSP, error) {
func loadCSPConfig(presetYamlContent, customYamlContent []byte) (*config.CSP, error) {
// substitute env vars and load to struct
gofig.WithOptions(gofig.ParseEnv)
gofig.AddDriver(yaml.Driver)
err := gofig.LoadSources("yaml", yamlContent)
presetMap := map[string]interface{}{}
err := yamlv3.Unmarshal(presetYamlContent, &presetMap)
if err != nil {
return nil, err
}
customMap := map[string]interface{}{}
err = yamlv3.Unmarshal(customYamlContent, &customMap)
if err != nil {
return nil, err
}
mergedMap := deepMerge(presetMap, customMap)
mergedYamlContent, err := yamlv3.Marshal(mergedMap)
if err != nil {
return nil, err
}
err = gofig.LoadSources("yaml", mergedYamlContent)
if err != nil {
return nil, err
}
@@ -41,11 +59,78 @@ func loadCSPConfig(yamlContent []byte) (*config.CSP, error) {
return &cspConfig, nil
}
func loadCSPYaml(proxyCfg *config.Config) ([]byte, error) {
if proxyCfg.CSPConfigFileLocation == "" {
return []byte(config.DefaultCSPConfig), nil
// deepMerge recursively merges map2 into map1.
// - nested maps are merged recursively
// - slices are concatenated, preserving order and avoiding duplicates
// - scalar or type-mismatched values from map2 overwrite map1
func deepMerge(map1, map2 map[string]interface{}) map[string]interface{} {
if map1 == nil {
out := make(map[string]interface{}, len(map2))
for k, v := range map2 {
out[k] = v
}
return out
}
return os.ReadFile(proxyCfg.CSPConfigFileLocation)
for k, v2 := range map2 {
if v1, ok := map1[k]; ok {
// both maps -> recurse
if m1, ok1 := v1.(map[string]interface{}); ok1 {
if m2, ok2 := v2.(map[string]interface{}); ok2 {
map1[k] = deepMerge(m1, m2)
continue
}
}
// both slices -> merge unique
if s1, ok1 := v1.([]interface{}); ok1 {
if s2, ok2 := v2.([]interface{}); ok2 {
merged := append([]interface{}{}, s1...)
for _, item := range s2 {
if !sliceContains(merged, item) {
merged = append(merged, item)
}
}
map1[k] = merged
continue
}
// s1 is slice, v2 single -> append if missing
if !sliceContains(s1, v2) {
map1[k] = append(s1, v2)
}
continue
}
// default: overwrite
map1[k] = v2
} else {
// new key -> just set
map1[k] = v2
}
}
return map1
}
func sliceContains(slice []interface{}, val interface{}) bool {
for _, v := range slice {
if reflect.DeepEqual(v, val) {
return true
}
}
return false
}
func loadCSPYaml(proxyCfg *config.Config) ([]byte, []byte, error) {
if proxyCfg.CSPConfigFileOverrideLocation != "" {
overrideCSPYaml, err := os.ReadFile(proxyCfg.CSPConfigFileOverrideLocation)
return overrideCSPYaml, []byte{}, err
}
if proxyCfg.CSPConfigFileLocation == "" {
return []byte(config.DefaultCSPConfig), nil, nil
}
customCSPYaml, err := os.ReadFile(proxyCfg.CSPConfigFileLocation)
return []byte(config.DefaultCSPConfig), customCSPYaml, err
}
// Security is a middleware to apply security relevant http headers like CSP.

View File

@@ -4,11 +4,12 @@ import (
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
)
func TestLoadCSPConfig(t *testing.T) {
// setup test env
yaml := `
presetYaml := `
directives:
frame-src:
- '''self'''
@@ -17,12 +18,23 @@ directives:
- 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}/'
`
config, err := loadCSPConfig([]byte(yaml))
customYaml := `
directives:
img-src:
- '''self'''
- 'data:'
frame-src:
- 'https://some.custom.domain/'
`
config, err := loadCSPConfig([]byte(presetYaml), []byte(customYaml))
if err != nil {
t.Error(err)
}
assert.Equal(t, config.Directives["frame-src"][0], "'self'")
assert.Equal(t, config.Directives["frame-src"][1], "https://embed.diagrams.net/")
assert.Equal(t, config.Directives["frame-src"][2], "https://onlyoffice.opencloud.test/")
assert.Equal(t, config.Directives["frame-src"][3], "https://collabora.opencloud.test/")
assert.Assert(t, cmp.Contains(config.Directives["frame-src"], "'self'"))
assert.Assert(t, cmp.Contains(config.Directives["frame-src"], "https://embed.diagrams.net/"))
assert.Assert(t, cmp.Contains(config.Directives["frame-src"], "https://onlyoffice.opencloud.test/"))
assert.Assert(t, cmp.Contains(config.Directives["frame-src"], "https://collabora.opencloud.test/"))
assert.Assert(t, cmp.Contains(config.Directives["img-src"], "'self'"))
assert.Assert(t, cmp.Contains(config.Directives["img-src"], "data:"))
}

View File

@@ -5,6 +5,7 @@ import (
"crypto/tls"
"fmt"
"net/http"
"os"
"os/signal"
"github.com/opencloud-eu/reva/v2/pkg/events/raw"
@@ -84,30 +85,37 @@ func Server(cfg *config.Config) *cli.Command {
eng = bleve.NewBackend(idx, bleveQuery.DefaultCreator, logger)
case "open-search":
client, err := opensearchgoAPI.NewClient(opensearchgoAPI.Config{
Client: opensearchgo.Config{
Addresses: cfg.Engine.OpenSearch.Client.Addresses,
Username: cfg.Engine.OpenSearch.Client.Username,
Password: cfg.Engine.OpenSearch.Client.Password,
Header: cfg.Engine.OpenSearch.Client.Header,
CACert: cfg.Engine.OpenSearch.Client.CACert,
RetryOnStatus: cfg.Engine.OpenSearch.Client.RetryOnStatus,
DisableRetry: cfg.Engine.OpenSearch.Client.DisableRetry,
EnableRetryOnTimeout: cfg.Engine.OpenSearch.Client.EnableRetryOnTimeout,
MaxRetries: cfg.Engine.OpenSearch.Client.MaxRetries,
CompressRequestBody: cfg.Engine.OpenSearch.Client.CompressRequestBody,
DiscoverNodesOnStart: cfg.Engine.OpenSearch.Client.DiscoverNodesOnStart,
DiscoverNodesInterval: cfg.Engine.OpenSearch.Client.DiscoverNodesInterval,
EnableMetrics: cfg.Engine.OpenSearch.Client.EnableMetrics,
EnableDebugLogger: cfg.Engine.OpenSearch.Client.EnableDebugLogger,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: cfg.Engine.OpenSearch.Client.Insecure,
},
clientConfig := opensearchgo.Config{
Addresses: cfg.Engine.OpenSearch.Client.Addresses,
Username: cfg.Engine.OpenSearch.Client.Username,
Password: cfg.Engine.OpenSearch.Client.Password,
Header: cfg.Engine.OpenSearch.Client.Header,
RetryOnStatus: cfg.Engine.OpenSearch.Client.RetryOnStatus,
DisableRetry: cfg.Engine.OpenSearch.Client.DisableRetry,
EnableRetryOnTimeout: cfg.Engine.OpenSearch.Client.EnableRetryOnTimeout,
MaxRetries: cfg.Engine.OpenSearch.Client.MaxRetries,
CompressRequestBody: cfg.Engine.OpenSearch.Client.CompressRequestBody,
DiscoverNodesOnStart: cfg.Engine.OpenSearch.Client.DiscoverNodesOnStart,
DiscoverNodesInterval: cfg.Engine.OpenSearch.Client.DiscoverNodesInterval,
EnableMetrics: cfg.Engine.OpenSearch.Client.EnableMetrics,
EnableDebugLogger: cfg.Engine.OpenSearch.Client.EnableDebugLogger,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: cfg.Engine.OpenSearch.Client.Insecure,
},
},
})
}
if cfg.Engine.OpenSearch.Client.CACert != "" {
certBytes, err := os.ReadFile(cfg.Engine.OpenSearch.Client.CACert)
if err != nil {
return fmt.Errorf("failed to read CA cert: %w", err)
}
clientConfig.CACert = certBytes
}
client, err := opensearchgoAPI.NewClient(opensearchgoAPI.Config{Client: clientConfig})
if err != nil {
return fmt.Errorf("failed to create OpenSearch client: %w", err)
}

View File

@@ -34,7 +34,7 @@ type EngineOpenSearchClient struct {
Username string `yaml:"username" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_USERNAME" desc:"Username for HTTP Basic Authentication." introductionVersion:"%%NEXT%%"`
Password string `yaml:"password" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_PASSWORD" desc:"Password for HTTP Basic Authentication." introductionVersion:"%%NEXT%%"`
Header http.Header `yaml:"header" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_HEADER" desc:"HTTP headers to include in requests." introductionVersion:"%%NEXT%%"`
CACert []byte `yaml:"ca_cert" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_CA_CERT" desc:"CA certificate for TLS connections." introductionVersion:"%%NEXT%%"`
CACert string `yaml:"ca_cert" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_CA_CERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the opensearch server." introductionVersion:"%%NEXT%%"`
RetryOnStatus []int `yaml:"retry_on_status" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_RETRY_ON_STATUS" desc:"HTTP status codes that trigger a retry." introductionVersion:"%%NEXT%%"`
DisableRetry bool `yaml:"disable_retry" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_DISABLE_RETRY" desc:"Disable retries on errors." introductionVersion:"%%NEXT%%"`
EnableRetryOnTimeout bool `yaml:"enable_retry_on_timeout" env:"SEARCH_ENGINE_OPEN_SEARCH_CLIENT_ENABLE_RETRY_ON_TIMEOUT" desc:"Enable retries on timeout." introductionVersion:"%%NEXT%%"`

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-11-07 00:02+0000\n"
"POT-Creation-Date: 2025-11-24 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"
@@ -103,7 +103,7 @@ msgstr "Ilmoita, kun avaruuden jäsenyys on vanhentunut"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr ""
msgstr "Poistettu avaruuden jäsenyydestä"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46

View File

@@ -0,0 +1,42 @@
# Sharing
The `sharing` service provides the CS3 Sharing API for OpenCloud. It manages user shares and public link shares, implementing the core sharing functionality.
## Overview
The sharing service handles:
- User-to-user shares (share a file or folder with another user)
- Public link shares (share via a public URL)
- Share permissions and roles
- Share lifecycle management (create, update, delete)
This service works in conjunction with the storage providers (`storage-shares` and `storage-publiclink`) to persist and manage share information.
## Integration
The sharing service integrates with:
- `frontend` and `ocs` - Provide HTTP APIs that translate to CS3 sharing calls
- `storage-shares` - Stores and manages received shares
- `storage-publiclink` - Manages public link shares
- `graph` - Provides LibreGraph API for sharing with roles
## Share Types
The service supports different types of shares:
- **User shares** - Share resources with specific users
- **Group shares** - Share resources with groups
- **Public link shares** - Create public URLs for sharing
- **Federated shares** - Share with users on other OpenCloud instances (via `ocm` service)
## Configuration
Share behavior can be configured via environment variables:
- Password enforcement for public shares
- Auto-acceptance of shares
- Share permissions and restrictions
See the `frontend` service README for more details on share-related configuration options.
## Scalability
The sharing service depends on the configured storage backends for share metadata. Scalability characteristics depend on the chosen storage backend configuration.

View File

@@ -0,0 +1,37 @@
# Storage PublicLink
The `storage-publiclink` service provides storage backend functionality for public link shares in OpenCloud. It implements the CS3 storage provider interface specifically for working with public link shared resources.
## Overview
This service is part of the storage services family and is responsible for:
- Providing access to publicly shared resources
- Handling anonymous access to shared content
## Integration
The storage-publiclink service integrates with:
- `sharing` service - Manages and persists public link shares
- `frontend` and `ocdav` - Provide HTTP/WebDAV access to public links
- Storage drivers - Accesses the actual file content
## Storage Registry
The service is registered in the gateway's storage registry with:
- Provider ID: `7993447f-687f-490d-875c-ac95e89a62a4`
- Mount point: `/public`
- Space types: `grant` and `mountpoint`
See the `gateway` README for more details on storage registry configuration.
## Access Control
Public link shares can be configured with:
- Password protection
- Expiration dates
- Read-only or read-write permissions
- Download limits
## Scalability
The storage-publiclink service can be scaled horizontally.

View File

@@ -0,0 +1,33 @@
# Storage Shares
The `storage-shares` service provides storage backend functionality for user and group shares in OpenCloud. It implements the CS3 storage provider interface specifically for working with shared resources.
## Overview
This service is part of the storage services family and is responsible for:
- Providing a virtual view of received shares
- Handling access to resources shared by other users
## Integration
The storage-shares service integrates with:
- `sharing` service - Manages and persists shares
- `storage-users` service - Accesses the underlying file content
- `frontend` and `ocdav` - Provide HTTP/WebDAV access to shares
## Virtual Shares Folder
The service provides a virtual "Shares" folder for each user where all received shares are mounted. This allows users to access all files and folders that have been shared with them in a centralized location.
## Storage Registry
The service is registered in the gateway's storage registry with:
- Provider ID: `a0ca6a90-a365-4782-871e-d44447bbc668`
- Mount point: `/users/{{.CurrentUser.Id.OpaqueId}}/Shares`
- Space types: `virtual`, `grant`, and `mountpoint`
See the `gateway` README for more details on storage registry configuration.
## Scalability
The storage-shares service can be scaled horizontally.

28
services/users/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Users
The `users` service provides the CS3 Users API for OpenCloud. It is responsible for managing user information and authentication within the OpenCloud instance.
This service implements the CS3 identity user provider interface, allowing other services to query and manage user accounts. It works as a backend provider for the `graph` service when using the CS3 backend mode.
## Backend Integration
The users service can work with different storage backends:
- LDAP integration through the graph service
- Direct CS3 API implementation
When using the `graph` service with the CS3 backend (`GRAPH_IDENTITY_BACKEND=cs3`), the graph service queries user information through this service.
## API
The service provides CS3 gRPC APIs for:
- Listing users
- Getting user information
- Finding users by username, email, or ID
## Usage
The users service is only used internally by other OpenCloud services and not being accessed directly by clients. The `frontend`, `ocs`, and `graph` services translate HTTP API requests into CS3 API calls to this service.
## Scalability
Since the users service queries backend systems (like LDAP through the configured identity backend), it can be scaled horizontally without additional configuration when using stateless backends.

View File

@@ -2701,6 +2701,28 @@ class FeatureContext extends BehatVariablesContext {
}
}
/**
* @AfterScenario
*
* @return void
*
* @throws Exception|GuzzleException
*/
public function cleanDataAfterTests(): void {
if (!OcHelper::isTestingOnReva() && !OcHelper::isUsingPreparedLdapUsers()) {
$this->spacesContext->deleteAllProjectSpaces();
}
if (OcHelper::isTestingOnReva()) {
OcHelper::deleteRevaUserData($this->getCreatedUsers());
}
if ($this->isTestingWithLdap()) {
$this->deleteLdapUsersAndGroups();
}
$this->cleanupDatabaseUsers();
$this->cleanupDatabaseGroups();
}
/**
* @BeforeScenario @temporary_storage_on_server
*

View File

@@ -1893,24 +1893,6 @@ trait Provisioning {
);
}
/**
* @AfterScenario
*
* @return void
* @throws Exception
*/
public function afterScenario(): void {
if (OcHelper::isTestingOnReva()) {
OcHelper::deleteRevaUserData($this->getCreatedUsers());
}
if ($this->isTestingWithLdap()) {
$this->deleteLdapUsersAndGroups();
}
$this->cleanupDatabaseUsers();
$this->cleanupDatabaseGroups();
}
/**
*
* @return void

View File

@@ -478,20 +478,6 @@ class SpacesContext implements Context {
$this->archiverContext = BehatHelper::getContext($scope, $environment, 'ArchiverContext');
}
/**
* @AfterScenario
*
* @return void
*
* @throws Exception|GuzzleException
*/
public function cleanDataAfterTests(): void {
if (OcHelper::isTestingOnReva() || OcHelper::isUsingPreparedLdapUsers()) {
return;
}
$this->deleteAllProjectSpaces();
}
/**
* the admin user first disables and then deletes spaces
*

View File

@@ -219,7 +219,7 @@ Feature: download file
And the following headers should be set
| header | value |
| Content-Disposition | attachment; filename*=UTF-8''<encoded-file-name>; filename="<file-name>" |
| Content-Security-Policy | child-src 'self'; connect-src 'self' blob: https://raw.githubusercontent.com/opencloud-eu/awesome-apps/; default-src 'none'; font-src 'self'; frame-ancestors 'self'; frame-src 'self' blob: https://embed.diagrams.net/; img-src 'self' data: blob: https://raw.githubusercontent.com/opencloud-eu/awesome-apps/; manifest-src 'self'; media-src 'self'; object-src 'self' blob:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' |
| Content-Security-Policy | child-src 'self'; connect-src 'self' blob: https://raw.githubusercontent.com/opencloud-eu/awesome-apps/ https://update.opencloud.eu/; default-src 'none'; font-src 'self'; frame-ancestors 'self'; frame-src 'self' blob: https://embed.diagrams.net/; img-src 'self' data: blob: https://raw.githubusercontent.com/opencloud-eu/awesome-apps/; manifest-src 'self'; media-src 'self'; object-src 'self' blob:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' |
| X-Content-Type-Options | nosniff |
| X-Frame-Options | SAMEORIGIN |
| X-Permitted-Cross-Domain-Policies | none |
@@ -247,7 +247,7 @@ Feature: download file
And the following headers should be set
| header | value |
| Content-Disposition | attachment; filename*=UTF-8''%22quote%22double%22.txt; filename=""quote"double".txt" |
| Content-Security-Policy | child-src 'self'; connect-src 'self' blob: https://raw.githubusercontent.com/opencloud-eu/awesome-apps/; default-src 'none'; font-src 'self'; frame-ancestors 'self'; frame-src 'self' blob: https://embed.diagrams.net/; img-src 'self' data: blob: https://raw.githubusercontent.com/opencloud-eu/awesome-apps/; manifest-src 'self'; media-src 'self'; object-src 'self' blob:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' |
| Content-Security-Policy | child-src 'self'; connect-src 'self' blob: https://raw.githubusercontent.com/opencloud-eu/awesome-apps/ https://update.opencloud.eu/; default-src 'none'; font-src 'self'; frame-ancestors 'self'; frame-src 'self' blob: https://embed.diagrams.net/; img-src 'self' data: blob: https://raw.githubusercontent.com/opencloud-eu/awesome-apps/; manifest-src 'self'; media-src 'self'; object-src 'self' blob:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' |
| X-Content-Type-Options | nosniff |
| X-Frame-Options | SAMEORIGIN |
| X-Permitted-Cross-Domain-Policies | none |

View File

@@ -82,8 +82,8 @@ func scoreSortFunc() func(i, j *search.DocumentMatch) int {
func getFusionExplAt(hit *search.DocumentMatch, i int, value float64, message string) *search.Explanation {
return &search.Explanation{
Value: value,
Message: message,
Value: value,
Message: message,
Children: []*search.Explanation{hit.Expl.Children[i]},
}
}

View File

@@ -388,3 +388,11 @@ type SynonymIndex interface {
// IndexSynonym indexes a synonym definition, with the specified id and belonging to the specified collection.
IndexSynonym(id string, collection string, definition *SynonymDefinition) error
}
type InsightsIndex interface {
Index
// TermFrequencies returns the tokens ordered by frequencies for the field index.
TermFrequencies(field string, limit int, descending bool) ([]index.TermFreq, error)
// CentroidCardinalities returns the centroids (clusters) from IVF indexes ordered by data density.
CentroidCardinalities(field string, limit int, desceding bool) ([]index.CentroidCardinality, error)
}

View File

@@ -23,6 +23,7 @@ import (
"path/filepath"
"reflect"
"sort"
"strings"
"sync"
"sync/atomic"
@@ -1234,3 +1235,61 @@ func (is *IndexSnapshot) MergeUpdateFieldsInfo(updatedFields map[string]*index.U
}
}
}
// TermFrequencies returns the top N terms ordered by the frequencies
// for a given field across all segments in the index snapshot.
func (is *IndexSnapshot) TermFrequencies(field string, limit int, descending bool) (
termFreqs []index.TermFreq, err error) {
if len(is.segment) == 0 {
return nil, nil
}
if limit <= 0 {
return nil, fmt.Errorf("limit must be positive")
}
// Use FieldDict which aggregates term frequencies across all segments
fieldDict, err := is.FieldDict(field)
if err != nil {
return nil, fmt.Errorf("failed to get field dictionary for field %s: %v", field, err)
}
defer fieldDict.Close()
// Preallocate slice with capacity equal to the number of unique terms
// in the field dictionary
termFreqs = make([]index.TermFreq, 0, fieldDict.Cardinality())
// Iterate through all terms using FieldDict
for {
dictEntry, err := fieldDict.Next()
if err != nil {
return nil, fmt.Errorf("error iterating field dictionary: %v", err)
}
if dictEntry == nil {
break // End of terms
}
termFreqs = append(termFreqs, index.TermFreq{
Term: dictEntry.Term,
Frequency: dictEntry.Count,
})
}
// Sort by frequency (descending or ascending)
sort.Slice(termFreqs, func(i, j int) bool {
if termFreqs[i].Frequency == termFreqs[j].Frequency {
// If frequencies are equal, sort by term lexicographically
return strings.Compare(termFreqs[i].Term, termFreqs[j].Term) < 0
}
if descending {
return termFreqs[i].Frequency > termFreqs[j].Frequency
}
return termFreqs[i].Frequency < termFreqs[j].Frequency
})
if limit >= len(termFreqs) {
return termFreqs, nil
}
return termFreqs[:limit], nil
}

View File

@@ -23,6 +23,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"sort"
"github.com/blevesearch/bleve/v2/size"
index "github.com/blevesearch/bleve_index_api"
@@ -167,3 +168,52 @@ func (i *IndexSnapshotVectorReader) Close() error {
// TODO Consider if any scope of recycling here.
return nil
}
func (i *IndexSnapshot) CentroidCardinalities(field string, limit int, descending bool) (
[]index.CentroidCardinality, error) {
if len(i.segment) == 0 {
return nil, nil
}
if limit <= 0 {
return nil, fmt.Errorf("limit must be positive")
}
centroids := make([]index.CentroidCardinality, 0, limit*len(i.segment))
for _, segment := range i.segment {
if sv, ok := segment.segment.(segment_api.VectorSegment); ok {
vecIndex, err := sv.InterpretVectorIndex(field,
false /* does not require filtering */, segment.deleted)
if err != nil {
return nil, fmt.Errorf("failed to interpret vector index for field %s in segment: %v", field, err)
}
centroidCardinalities, err := vecIndex.ObtainKCentroidCardinalitiesFromIVFIndex(limit, descending)
if err != nil {
return nil, fmt.Errorf("failed to obtain top k centroid cardinalities for field %s in segment: %v", field, err)
}
if len(centroidCardinalities) > 0 {
centroids = append(centroids, centroidCardinalities...)
}
}
}
if len(centroids) == 0 {
return nil, nil
}
sort.Slice(centroids, func(i, j int) bool {
if descending {
return centroids[i].Cardinality > centroids[j].Cardinality
}
return centroids[i].Cardinality < centroids[j].Cardinality
})
if limit >= len(centroids) {
return centroids, nil
}
return centroids[:limit], nil
}

View File

@@ -17,6 +17,8 @@ package bleve
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"
@@ -1136,3 +1138,159 @@ func (f *indexAliasImplFieldDict) Close() error {
func (f *indexAliasImplFieldDict) Cardinality() int {
return f.fieldDict.Cardinality()
}
// -----------------------------------------------------------------------------
func (i *indexAliasImpl) TermFrequencies(field string, limit int, descending bool) (
[]index.TermFreq, error) {
i.mutex.RLock()
defer i.mutex.RUnlock()
if !i.open {
return nil, ErrorIndexClosed
}
if len(i.indexes) < 1 {
return nil, ErrorAliasEmpty
}
// short circuit the simple case
if len(i.indexes) == 1 {
if idx, ok := i.indexes[0].(InsightsIndex); ok {
return idx.TermFrequencies(field, limit, descending)
}
return nil, nil
}
// run search on each index in separate go routine
var waitGroup sync.WaitGroup
asyncResults := make(chan []index.TermFreq, len(i.indexes))
searchChildIndex := func(in Index, field string, limit int, descending bool) {
var rv []index.TermFreq
if idx, ok := in.(InsightsIndex); ok {
// over sample for higher accuracy
rv, _ = idx.TermFrequencies(field, limit*5, descending)
}
asyncResults <- rv
waitGroup.Done()
}
waitGroup.Add(len(i.indexes))
for _, in := range i.indexes {
go searchChildIndex(in, field, limit, descending)
}
// on another go routine, close after finished
go func() {
waitGroup.Wait()
close(asyncResults)
}()
rvTermFreqsMap := make(map[string]uint64)
for asr := range asyncResults {
for _, entry := range asr {
rvTermFreqsMap[entry.Term] += entry.Frequency
}
}
rvTermFreqs := make([]index.TermFreq, 0, len(rvTermFreqsMap))
for term, freq := range rvTermFreqsMap {
rvTermFreqs = append(rvTermFreqs, index.TermFreq{
Term: term,
Frequency: freq,
})
}
if descending {
sort.Slice(rvTermFreqs, func(i, j int) bool {
if rvTermFreqs[i].Frequency == rvTermFreqs[j].Frequency {
// If frequencies are equal, sort by term lexicographically
return strings.Compare(rvTermFreqs[i].Term, rvTermFreqs[j].Term) < 0
}
return rvTermFreqs[i].Frequency > rvTermFreqs[j].Frequency
})
} else {
sort.Slice(rvTermFreqs, func(i, j int) bool {
if rvTermFreqs[i].Frequency == rvTermFreqs[j].Frequency {
// If frequencies are equal, sort by term lexicographically
return strings.Compare(rvTermFreqs[i].Term, rvTermFreqs[j].Term) < 0
}
return rvTermFreqs[i].Frequency < rvTermFreqs[j].Frequency
})
}
if limit > len(rvTermFreqs) {
limit = len(rvTermFreqs)
}
return rvTermFreqs[:limit], nil
}
func (i *indexAliasImpl) CentroidCardinalities(field string, limit int, descending bool) (
[]index.CentroidCardinality, error) {
i.mutex.RLock()
defer i.mutex.RUnlock()
if !i.open {
return nil, ErrorIndexClosed
}
if len(i.indexes) < 1 {
return nil, ErrorAliasEmpty
}
// short circuit the simple case
if len(i.indexes) == 1 {
if idx, ok := i.indexes[0].(InsightsIndex); ok {
return idx.CentroidCardinalities(field, limit, descending)
}
return nil, nil
}
// run search on each index in separate go routine
var waitGroup sync.WaitGroup
asyncResults := make(chan []index.CentroidCardinality, len(i.indexes))
searchChildIndex := func(in Index, field string, limit int, descending bool) {
var rv []index.CentroidCardinality
if idx, ok := in.(InsightsIndex); ok {
rv, _ = idx.CentroidCardinalities(field, limit, descending)
}
asyncResults <- rv
waitGroup.Done()
}
waitGroup.Add(len(i.indexes))
for _, in := range i.indexes {
go searchChildIndex(in, field, limit, descending)
}
// on another go routine, close after finished
go func() {
waitGroup.Wait()
close(asyncResults)
}()
rvCentroidCardinalitiesResult := make([]index.CentroidCardinality, 0, limit)
for asr := range asyncResults {
asr = append(asr, rvCentroidCardinalitiesResult...)
if descending {
sort.Slice(asr, func(i, j int) bool {
return asr[i].Cardinality > asr[j].Cardinality
})
} else {
sort.Slice(asr, func(i, j int) bool {
return asr[i].Cardinality < asr[j].Cardinality
})
}
if limit > len(asr) {
limit = len(asr)
}
rvCentroidCardinalitiesResult = asr[:limit]
}
return rvCentroidCardinalitiesResult, nil
}

View File

@@ -57,8 +57,6 @@ type indexImpl struct {
const storePath = "store"
var mappingInternalKey = []byte("_mapping")
const (
SearchQueryStartCallbackKey search.ContextKey = "_search_query_start_callback_key"
SearchQueryEndCallbackKey search.ContextKey = "_search_query_end_callback_key"
@@ -641,8 +639,57 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
}
}
// ------------------------------------------------------------------------------------------
// set up additional contexts for any search operation that will proceed from
// here, such as presearch, collectors etc.
// Scoring model callback to be used to get scoring model
scoringModelCallback := func() string {
if isBM25Enabled(i.m) {
return index.BM25Scoring
}
return index.DefaultScoringModel
}
ctx = context.WithValue(ctx, search.GetScoringModelCallbackKey,
search.GetScoringModelCallbackFn(scoringModelCallback))
// This callback and variable handles the tracking of bytes read
// 1. as part of creation of tfr and its Next() calls which is
// accounted by invoking this callback when the TFR is closed.
// 2. the docvalues portion (accounted in collector) and the retrieval
// of stored fields bytes (by LoadAndHighlightFields)
var totalSearchCost uint64
sendBytesRead := func(bytesRead uint64) {
totalSearchCost += bytesRead
}
// Ensure IO cost accounting and result cost assignment happen on all return paths
defer func() {
if sr != nil {
sr.Cost = totalSearchCost
}
if is, ok := indexReader.(*scorch.IndexSnapshot); ok {
is.UpdateIOStats(totalSearchCost)
}
search.RecordSearchCost(ctx, search.DoneM, 0)
}()
ctx = context.WithValue(ctx, search.SearchIOStatsCallbackKey, search.SearchIOStatsCallbackFunc(sendBytesRead))
// Geo buffer pool callback to be used for getting geo buffer pool
var bufPool *s2.GeoBufferPool
getBufferPool := func() *s2.GeoBufferPool {
if bufPool == nil {
bufPool = s2.NewGeoBufferPool(search.MaxGeoBufPoolSize, search.MinGeoBufPoolSize)
}
return bufPool
}
ctx = context.WithValue(ctx, search.GeoBufferPoolCallbackKey, search.GeoBufferPoolCallbackFunc(getBufferPool))
// ------------------------------------------------------------------------------------------
if _, ok := ctx.Value(search.PreSearchKey).(bool); ok {
preSearchResult, err := i.preSearch(ctx, req, indexReader)
sr, err = i.preSearch(ctx, req, indexReader)
if err != nil {
return nil, err
}
@@ -656,7 +703,8 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
// time stat
searchDuration := time.Since(searchStart)
atomic.AddUint64(&i.stats.searchTime, uint64(searchDuration))
return preSearchResult, nil
return sr, nil
}
var reverseQueryExecution bool
@@ -726,6 +774,9 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
// if score fusion, run collect if rescorer is defined
if rescorer != nil && requestHasKNN(req) {
knnHits, err = i.runKnnCollector(ctx, req, indexReader, false)
if err != nil {
return nil, err
}
}
}
@@ -745,7 +796,6 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
if !contextScoreFusionKeyExists {
setKnnHitsInCollector(knnHits, req, coll)
}
if fts != nil {
if is, ok := indexReader.(*scorch.IndexSnapshot); ok {
@@ -754,44 +804,12 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
ctx = context.WithValue(ctx, search.FieldTermSynonymMapKey, fts)
}
scoringModelCallback := func() string {
if isBM25Enabled(i.m) {
return index.BM25Scoring
}
return index.DefaultScoringModel
}
ctx = context.WithValue(ctx, search.GetScoringModelCallbackKey,
search.GetScoringModelCallbackFn(scoringModelCallback))
// set the bm25Stats (stats important for consistent scoring) in
// the context object
if bm25Stats != nil {
ctx = context.WithValue(ctx, search.BM25StatsKey, bm25Stats)
}
// This callback and variable handles the tracking of bytes read
// 1. as part of creation of tfr and its Next() calls which is
// accounted by invoking this callback when the TFR is closed.
// 2. the docvalues portion (accounted in collector) and the retrieval
// of stored fields bytes (by LoadAndHighlightFields)
var totalSearchCost uint64
sendBytesRead := func(bytesRead uint64) {
totalSearchCost += bytesRead
}
ctx = context.WithValue(ctx, search.SearchIOStatsCallbackKey, search.SearchIOStatsCallbackFunc(sendBytesRead))
var bufPool *s2.GeoBufferPool
getBufferPool := func() *s2.GeoBufferPool {
if bufPool == nil {
bufPool = s2.NewGeoBufferPool(search.MaxGeoBufPoolSize, search.MinGeoBufPoolSize)
}
return bufPool
}
ctx = context.WithValue(ctx, search.GeoBufferPoolCallbackKey, search.GeoBufferPoolCallbackFunc(getBufferPool))
searcher, err := req.Query.Searcher(ctx, indexReader, i.m, search.SearcherOptions{
Explain: req.Explain,
IncludeTermVectors: req.IncludeLocations || req.Highlight != nil,
@@ -804,14 +822,6 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
if serr := searcher.Close(); err == nil && serr != nil {
err = serr
}
if sr != nil {
sr.Cost = totalSearchCost
}
if sr, ok := indexReader.(*scorch.IndexSnapshot); ok {
sr.UpdateIOStats(totalSearchCost)
}
search.RecordSearchCost(ctx, search.DoneM, 0)
}()
if req.Facets != nil {
@@ -1388,3 +1398,68 @@ func (i *indexImpl) FireIndexEvent() {
internalEventIndex.FireIndexEvent()
}
}
// -----------------------------------------------------------------------------
func (i *indexImpl) TermFrequencies(field string, limit int, descending bool) (
[]index.TermFreq, error) {
i.mutex.RLock()
defer i.mutex.RUnlock()
if !i.open {
return nil, ErrorIndexClosed
}
reader, err := i.i.Reader()
if err != nil {
return nil, err
}
defer func() {
if cerr := reader.Close(); err == nil && cerr != nil {
err = cerr
}
}()
insightsReader, ok := reader.(index.IndexInsightsReader)
if !ok {
return nil, fmt.Errorf("index reader does not support TermFrequencies")
}
return insightsReader.TermFrequencies(field, limit, descending)
}
func (i *indexImpl) CentroidCardinalities(field string, limit int, descending bool) (
[]index.CentroidCardinality, error) {
i.mutex.RLock()
defer i.mutex.RUnlock()
if !i.open {
return nil, ErrorIndexClosed
}
reader, err := i.i.Reader()
if err != nil {
return nil, err
}
defer func() {
if cerr := reader.Close(); err == nil && cerr != nil {
err = cerr
}
}()
insightsReader, ok := reader.(index.IndexInsightsReader)
if !ok {
return nil, fmt.Errorf("index reader does not support CentroidCardinalities")
}
centroidCardinalities, err := insightsReader.CentroidCardinalities(field, limit, descending)
if err != nil {
return nil, err
}
for j := 0; j < len(centroidCardinalities); j++ {
centroidCardinalities[j].Index = i.name
}
return centroidCardinalities, nil
}

View File

@@ -755,4 +755,3 @@ func ParseParams(r *SearchRequest, input []byte) (*RequestParams, error) {
return params, nil
}

View File

@@ -185,17 +185,36 @@ func (q *BooleanQuery) Searcher(ctx context.Context, i index.IndexReader, m mapp
if err != nil {
return nil, err
}
var init bool
var refDoc *search.DocumentMatch
filterFunc = func(sctx *search.SearchContext, d *search.DocumentMatch) bool {
// Attempt to advance the filter searcher to the document identified by
// the base searcher's (unfiltered boolean) current result (d.IndexInternalID).
//
// If the filter searcher successfully finds a document with the same
// internal ID, it means the document satisfies the filter and should be kept.
//
// If the filter searcher returns an error, does not find a matching document,
// or finds a document with a different internal ID, the document should be discarded.
dm, err := filterSearcher.Advance(sctx, d.IndexInternalID)
return err == nil && dm != nil && bytes.Equal(dm.IndexInternalID, d.IndexInternalID)
// Initialize the reference document to point
// to the first document in the filterSearcher
var err error
if !init {
refDoc, err = filterSearcher.Next(sctx)
if err != nil {
return false
}
init = true
}
if refDoc == nil {
// filterSearcher is exhausted, d is not in filter
return false
}
// Compare document IDs
cmp := bytes.Compare(refDoc.IndexInternalID, d.IndexInternalID)
if cmp < 0 {
// filterSearcher is behind the current document, Advance() it
refDoc, err = filterSearcher.Advance(sctx, d.IndexInternalID)
if err != nil || refDoc == nil {
return false
}
// After advance, check if they're now equal
return bytes.Equal(refDoc.IndexInternalID, d.IndexInternalID)
}
// cmp >= 0: either equal (match) or filterSearcher is ahead (no match)
return cmp == 0
}
}

View File

@@ -431,6 +431,10 @@ func expandQuery(m mapping.IndexMapping, query Query) (Query, error) {
if err != nil {
return nil, err
}
q.Filter, err = expand(q.Filter)
if err != nil {
return nil, err
}
return q, nil
default:
return query, nil
@@ -481,7 +485,7 @@ func ExtractFields(q Query, m mapping.IndexMapping, fs FieldSet) (FieldSet, erro
fs, err = ExtractFields(expandedQuery, m, fs)
}
case *BooleanQuery:
for _, subq := range []Query{q.Must, q.Should, q.MustNot} {
for _, subq := range []Query{q.Must, q.Should, q.MustNot, q.Filter} {
fs, err = ExtractFields(subq, m, fs)
if err != nil {
break
@@ -553,6 +557,10 @@ func ExtractSynonyms(ctx context.Context, m mapping.SynonymMapping, r index.Thes
if err != nil {
return nil, err
}
rv, err = ExtractSynonyms(ctx, m, r, q.Filter, rv)
if err != nil {
return nil, err
}
case *ConjunctionQuery:
for _, child := range q.Conjuncts {
rv, err = ExtractSynonyms(ctx, m, r, child, rv)

View File

@@ -365,3 +365,29 @@ type EligibleDocumentSelector interface {
// This must be called after all eligible documents have been added.
SegmentEligibleDocs(segmentID int) []uint64
}
// -----------------------------------------------------------------------------
type TermFreq struct {
Term string `json:"term"`
Frequency uint64 `json:"frequency"`
}
type CentroidCardinality struct {
Index string `json:"index"`
Centroid []float32 `json:"centroid"`
Cardinality uint64 `json:"cardinality"`
}
// IndexInsightsReader is an extended index reader that supports APIs which can advertise
// details about content held within the index.
type IndexInsightsReader interface {
IndexReader
// Obtains a maximum limit number of indexed tokens for the field sorted based on frequencies.
TermFrequencies(field string, limit int, descending bool) (termFreqs []TermFreq, err error)
// Obtains a maximum limit number of centroid vectors from IVF indexes sorted based on
// cluster densities (or cardinalities)
CentroidCardinalities(field string, limit int, descending bool) (cenCards []CentroidCardinality, err error)
}

View File

@@ -14,6 +14,7 @@ import "C"
import (
"encoding/json"
"fmt"
"sort"
"unsafe"
)
@@ -64,6 +65,10 @@ type Index interface {
ObtainClustersWithDistancesFromIVFIndex(x []float32, centroidIDs []int64) (
[]int64, []float32, error)
// Applicable only to IVF indexes: Returns the top k centroid cardinalities and
// their vectors in chosen order (descending or ascending)
ObtainKCentroidCardinalitiesFromIVFIndex(limit int, descending bool) ([]uint64, [][]float32, error)
// Search queries the index with the vectors in x.
// Returns the IDs of the k nearest neighbors for each query vector and the
// corresponding distances.
@@ -214,6 +219,72 @@ func (idx *faissIndex) ObtainClustersWithDistancesFromIVFIndex(x []float32, cent
return centroids, centroidDistances, nil
}
func (idx *faissIndex) ObtainKCentroidCardinalitiesFromIVFIndex(limit int, descending bool) (
[]uint64, [][]float32, error) {
if limit <= 0 {
return nil, nil, nil
}
nlist := int(C.faiss_IndexIVF_nlist(idx.idx))
if nlist == 0 {
return nil, nil, nil
}
centroidCardinalities := make([]C.size_t, nlist)
// Allocate a flat buffer for all centroids, then slice it per centroid
d := idx.D()
flatCentroids := make([]float32, nlist*d)
// Call the C function to fill centroid vectors and cardinalities
c := C.faiss_IndexIVF_get_centroids_and_cardinality(
idx.idx,
(*C.float)(&flatCentroids[0]),
(*C.size_t)(&centroidCardinalities[0]),
nil,
)
if c != 0 {
return nil, nil, getLastError()
}
topIndices := getIndicesOfKCentroidCardinalities(
centroidCardinalities,
min(limit, nlist),
descending)
rvCardinalities := make([]uint64, len(topIndices))
rvCentroids := make([][]float32, len(topIndices))
for i, idx := range topIndices {
rvCardinalities[i] = uint64(centroidCardinalities[idx])
rvCentroids[i] = flatCentroids[idx*d : (idx+1)*d]
}
return rvCardinalities, rvCentroids, nil
}
func getIndicesOfKCentroidCardinalities(cardinalities []C.size_t, k int, descending bool) []int {
n := len(cardinalities)
indices := make([]int, n)
for i := range indices {
indices[i] = i
}
// Sort only the indices based on cardinality values
sort.Slice(indices, func(i, j int) bool {
if descending {
return cardinalities[indices[i]] > cardinalities[indices[j]]
}
return cardinalities[indices[i]] < cardinalities[indices[j]]
})
if k >= n {
return indices
}
return indices[:k]
}
func (idx *faissIndex) SearchClustersFromIVFIndex(selector Selector,
eligibleCentroidIDs []int64, minEligibleCentroids int, k int64, x,
centroidDis []float32, params json.RawMessage) ([]float32, []int64, error) {

View File

@@ -20,6 +20,7 @@ package segment
import (
"encoding/json"
index "github.com/blevesearch/bleve_index_api"
"github.com/RoaringBitmap/roaring/v2"
)
@@ -64,6 +65,8 @@ type VectorIndex interface {
params json.RawMessage) (VecPostingsList, error)
Close()
Size() uint64
ObtainKCentroidCardinalitiesFromIVFIndex(limit int, descending bool) ([]index.CentroidCardinality, error)
}
type VectorSegment interface {

View File

@@ -26,6 +26,7 @@ import (
"github.com/RoaringBitmap/roaring/v2"
"github.com/RoaringBitmap/roaring/v2/roaring64"
"github.com/bits-and-blooms/bitset"
index "github.com/blevesearch/bleve_index_api"
faiss "github.com/blevesearch/go-faiss"
segment "github.com/blevesearch/scorch_segment_api/v2"
)
@@ -279,6 +280,9 @@ type vectorIndexWrapper struct {
params json.RawMessage) (segment.VecPostingsList, error)
close func()
size func() uint64
obtainKCentroidCardinalitiesFromIVFIndex func(limit int, descending bool) (
[]index.CentroidCardinality, error)
}
func (i *vectorIndexWrapper) Search(qVector []float32, k int64,
@@ -301,6 +305,11 @@ func (i *vectorIndexWrapper) Size() uint64 {
return i.size()
}
func (i *vectorIndexWrapper) ObtainKCentroidCardinalitiesFromIVFIndex(limit int, descending bool) (
[]index.CentroidCardinality, error) {
return i.obtainKCentroidCardinalitiesFromIVFIndex(limit, descending)
}
// InterpretVectorIndex returns a construct of closures (vectorIndexWrapper)
// that will allow the caller to -
// (1) search within an attached vector index
@@ -520,6 +529,24 @@ func (sb *SegmentBase) InterpretVectorIndex(field string, requiresFiltering bool
size: func() uint64 {
return vecIndexSize
},
obtainKCentroidCardinalitiesFromIVFIndex: func(limit int, descending bool) ([]index.CentroidCardinality, error) {
if vecIndex == nil || !vecIndex.IsIVFIndex() {
return nil, nil
}
cardinalities, centroids, err := vecIndex.ObtainKCentroidCardinalitiesFromIVFIndex(limit, descending)
if err != nil {
return nil, err
}
centroidCardinalities := make([]index.CentroidCardinality, len(cardinalities))
for i, cardinality := range cardinalities {
centroidCardinalities[i] = index.CentroidCardinality{
Centroid: centroids[i],
Cardinality: cardinality,
}
}
return centroidCardinalities, nil
},
}
err error

View File

@@ -0,0 +1 @@
.DS_Store

37
vendor/github.com/clipperhouse/displaywidth/AGENTS.md generated vendored Normal file
View File

@@ -0,0 +1,37 @@
The goals and overview of this package can be found in the README.md file,
start by reading that.
The goal of this package is to determine the display (column) width of a
string, UTF-8 bytes, or runes, as would happen in a monospace font, especially
in a terminal.
When troubleshooting, write Go unit tests instead of executing debug scripts.
The tests can return whatever logs or output you need. If those tests are
only for temporary troubleshooting, clean up the tests after the debugging is
done.
(Separate executable debugging scripts are messy, tend to have conflicting
dependencies and are hard to cleanup.)
If you make changes to the trie generation in internal/gen, it can be invoked
by running `go generate` from the top package directory.
## Pull Requests and branches
For PRs (pull requests), you can use the gh CLI tool to retrieve details,
or post comments. Then, compare the current branch with main. Reviewing a PR
and reviewing a branch are about the same, but the PR may add context.
Look for bugs. Think like GitHub Copilot or Cursor BugBot.
Offer to post a brief summary of the review to the PR, via the gh CLI tool.
## Comparisons to go-runewidth
We originally attempted to make this package compatible with go-runewidth.
However, we found that there were too many differences in the handling of
certain characters and properties.
We believe, preliminarily, that our choices are more correct and complete,
by using more complete categories such as Unicode Cf (format) for zero-width
and Mn (Nonspacing_Mark) for combining marks.

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Oliver Kuederle
Copyright (c) 2025 Matt Sherman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

115
vendor/github.com/clipperhouse/displaywidth/README.md generated vendored Normal file
View File

@@ -0,0 +1,115 @@
# displaywidth
A high-performance Go package for measuring the monospace display width of strings, UTF-8 bytes, and runes.
[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/displaywidth.svg)](https://pkg.go.dev/github.com/clipperhouse/displaywidth)
[![Test](https://github.com/clipperhouse/displaywidth/actions/workflows/gotest.yml/badge.svg)](https://github.com/clipperhouse/displaywidth/actions/workflows/gotest.yml)
[![Fuzz](https://github.com/clipperhouse/displaywidth/actions/workflows/gofuzz.yml/badge.svg)](https://github.com/clipperhouse/displaywidth/actions/workflows/gofuzz.yml)
## Install
```bash
go get github.com/clipperhouse/displaywidth
```
## Usage
```go
package main
import (
"fmt"
"github.com/clipperhouse/displaywidth"
)
func main() {
width := displaywidth.String("Hello, 世界!")
fmt.Println(width)
width = displaywidth.Bytes([]byte("🌍"))
fmt.Println(width)
width = displaywidth.Rune('🌍')
fmt.Println(width)
}
```
### Options
You can specify East Asian Width and Strict Emoji Neutral settings. If
unspecified, the default is `EastAsianWidth: false, StrictEmojiNeutral: true`.
```go
options := displaywidth.Options{
EastAsianWidth: true,
StrictEmojiNeutral: false,
}
width := options.String("Hello, 世界!")
fmt.Println(width)
```
## Details
This package implements the Unicode East Asian Width standard (UAX #11) and is
intended to be compatible with `go-runewidth`. It operates on bytes without
decoding runes for better performance.
## Prior Art
[mattn/go-runewidth](https://github.com/mattn/go-runewidth)
[x/text/width](https://pkg.go.dev/golang.org/x/text/width)
[x/text/internal/triegen](https://pkg.go.dev/golang.org/x/text/internal/triegen)
## Benchmarks
Part of my motivation is the insight that we can avoid decoding runes for better performance.
```bash
go test -bench=. -benchmem
```
```
goos: darwin
goarch: arm64
pkg: github.com/clipperhouse/displaywidth
cpu: Apple M2
BenchmarkStringDefault/displaywidth-8 10537 ns/op 160.10 MB/s 0 B/op 0 allocs/op
BenchmarkStringDefault/go-runewidth-8 14162 ns/op 119.12 MB/s 0 B/op 0 allocs/op
BenchmarkString_EAW/displaywidth-8 10776 ns/op 156.55 MB/s 0 B/op 0 allocs/op
BenchmarkString_EAW/go-runewidth-8 23987 ns/op 70.33 MB/s 0 B/op 0 allocs/op
BenchmarkString_StrictEmoji/displaywidth-8 10892 ns/op 154.88 MB/s 0 B/op 0 allocs/op
BenchmarkString_StrictEmoji/go-runewidth-8 14552 ns/op 115.93 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/displaywidth-8 1116 ns/op 114.72 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/go-runewidth-8 1178 ns/op 108.67 MB/s 0 B/op 0 allocs/op
BenchmarkString_Unicode/displaywidth-8 896.9 ns/op 148.29 MB/s 0 B/op 0 allocs/op
BenchmarkString_Unicode/go-runewidth-8 1434 ns/op 92.72 MB/s 0 B/op 0 allocs/op
BenchmarkStringWidth_Emoji/displaywidth-8 3033 ns/op 238.74 MB/s 0 B/op 0 allocs/op
BenchmarkStringWidth_Emoji/go-runewidth-8 4841 ns/op 149.56 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/displaywidth-8 4064 ns/op 124.74 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/go-runewidth-8 4696 ns/op 107.97 MB/s 0 B/op 0 allocs/op
BenchmarkString_ControlChars/displaywidth-8 320.6 ns/op 102.93 MB/s 0 B/op 0 allocs/op
BenchmarkString_ControlChars/go-runewidth-8 373.8 ns/op 88.28 MB/s 0 B/op 0 allocs/op
BenchmarkRuneDefault/displaywidth-8 335.5 ns/op 411.35 MB/s 0 B/op 0 allocs/op
BenchmarkRuneDefault/go-runewidth-8 681.2 ns/op 202.58 MB/s 0 B/op 0 allocs/op
BenchmarkRuneWidth_EAW/displaywidth-8 146.7 ns/op 374.80 MB/s 0 B/op 0 allocs/op
BenchmarkRuneWidth_EAW/go-runewidth-8 495.6 ns/op 110.98 MB/s 0 B/op 0 allocs/op
BenchmarkRuneWidth_ASCII/displaywidth-8 63.00 ns/op 460.33 MB/s 0 B/op 0 allocs/op
BenchmarkRuneWidth_ASCII/go-runewidth-8 68.90 ns/op 420.91 MB/s 0 B/op 0 allocs/op
```
I use a similar technique in [this grapheme cluster library](https://github.com/clipperhouse/uax29).
## Compatibility
`displaywidth` will mostly give the same outputs as `go-runewidth`, but there are some differences:
- Unicode category Mn (Nonspacing Mark): `displaywidth` will return width 0, `go-runewidth` may return width 1 for some runes.
- Unicode category Cf (Format): `displaywidth` will return width 0, `go-runewidth` may return width 1 for some runes.
- Unicode category Mc (Spacing Mark): `displaywidth` will return width 1, `go-runewidth` may return width 0 for some runes.
- Unicode category Cs (Surrogate): `displaywidth` will return width 0, `go-runewidth` may return width 1 for some runes. Surrogates are not valid UTF-8; some packages may turn them into the replacement character (U+FFFD).
- Unicode category Zl (Line separator): `displaywidth` will return width 0, `go-runewidth` may return width 1.
- Unicode category Zp (Paragraph separator): `displaywidth` will return width 0, `go-runewidth` may return width 1.
- Unicode Noncharacters (U+FFFE and U+FFFF): `displaywidth` will return width 0, `go-runewidth` may return width 1.
See `TestCompatibility` for more details.

3
vendor/github.com/clipperhouse/displaywidth/gen.go generated vendored Normal file
View File

@@ -0,0 +1,3 @@
package displaywidth
//go:generate go run -C internal/gen .

1897
vendor/github.com/clipperhouse/displaywidth/trie.go generated vendored Normal file
View File

File diff suppressed because it is too large Load Diff

159
vendor/github.com/clipperhouse/displaywidth/width.go generated vendored Normal file
View File

@@ -0,0 +1,159 @@
package displaywidth
import (
"unicode/utf8"
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/uax29/v2/graphemes"
)
// String calculates the display width of a string
// using the [DefaultOptions]
func String(s string) int {
return DefaultOptions.String(s)
}
// Bytes calculates the display width of a []byte
// using the [DefaultOptions]
func Bytes(s []byte) int {
return DefaultOptions.Bytes(s)
}
func Rune(r rune) int {
return DefaultOptions.Rune(r)
}
type Options struct {
EastAsianWidth bool
StrictEmojiNeutral bool
}
var DefaultOptions = Options{
EastAsianWidth: false,
StrictEmojiNeutral: true,
}
// String calculates the display width of a string
// for the given options
func (options Options) String(s string) int {
if len(s) == 0 {
return 0
}
total := 0
g := graphemes.FromString(s)
for g.Next() {
// The first character in the grapheme cluster determines the width;
// modifiers and joiners do not contribute to the width.
props, _ := lookupProperties(g.Value())
total += props.width(options)
}
return total
}
// BytesOptions calculates the display width of a []byte
// for the given options
func (options Options) Bytes(s []byte) int {
if len(s) == 0 {
return 0
}
total := 0
g := graphemes.FromBytes(s)
for g.Next() {
// The first character in the grapheme cluster determines the width;
// modifiers and joiners do not contribute to the width.
props, _ := lookupProperties(g.Value())
total += props.width(options)
}
return total
}
func (options Options) Rune(r rune) int {
// Fast path for ASCII
if r < utf8.RuneSelf {
if isASCIIControl(byte(r)) {
// Control (0x00-0x1F) and DEL (0x7F)
return 0
}
// ASCII printable (0x20-0x7E)
return 1
}
// Surrogates (U+D800-U+DFFF) are invalid UTF-8 and have zero width
// Other packages might turn them into the replacement character (U+FFFD)
// in which case, we won't see it.
if r >= 0xD800 && r <= 0xDFFF {
return 0
}
// Stack-allocated to avoid heap allocation
var buf [4]byte // UTF-8 is at most 4 bytes
n := utf8.EncodeRune(buf[:], r)
// Skip the grapheme iterator and directly lookup properties
props, _ := lookupProperties(buf[:n])
return props.width(options)
}
func isASCIIControl(b byte) bool {
return b < 0x20 || b == 0x7F
}
const defaultWidth = 1
// is returns true if the property flag is set
func (p property) is(flag property) bool {
return p&flag != 0
}
// lookupProperties returns the properties for the first character in a string
func lookupProperties[T stringish.Interface](s T) (property, int) {
if len(s) == 0 {
return 0, 0
}
// Fast path for ASCII characters (single byte)
b := s[0]
if b < utf8.RuneSelf { // Single-byte ASCII
if isASCIIControl(b) {
// Control characters (0x00-0x1F) and DEL (0x7F) - width 0
return _ZeroWidth, 1
}
// ASCII printable characters (0x20-0x7E) - width 1
// Return 0 properties, width calculation will default to 1
return 0, 1
}
// Use the generated trie for lookup
props, size := lookup(s)
return property(props), size
}
// width determines the display width of a character based on its properties
// and configuration options
func (p property) width(options Options) int {
if p == 0 {
// Character not in trie, use default behavior
return defaultWidth
}
if p.is(_ZeroWidth) {
return 0
}
if options.EastAsianWidth {
if p.is(_East_Asian_Ambiguous) {
return 2
}
if p.is(_East_Asian_Ambiguous|_Emoji) && !options.StrictEmojiNeutral {
return 2
}
}
if p.is(_East_Asian_Full_Wide) {
return 2
}
// Default width for all other characters
return defaultWidth
}

2
vendor/github.com/clipperhouse/stringish/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
.DS_Store
*.test

21
vendor/github.com/clipperhouse/stringish/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Matt Sherman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

64
vendor/github.com/clipperhouse/stringish/README.md generated vendored Normal file
View File

@@ -0,0 +1,64 @@
# stringish
A small Go module that provides a generic type constraint for “string-like”
data, and a utf8 package that works with both strings and byte slices
without conversions.
```go
type Interface interface {
~[]byte | ~string
}
```
[![Go Reference](https://pkg.go.dev/badge/github.com/clipperhouse/stringish/utf8.svg)](https://pkg.go.dev/github.com/clipperhouse/stringish/utf8)
[![Test Status](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml/badge.svg)](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml)
## Install
```
go get github.com/clipperhouse/stringish
```
## Examples
```go
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/stringish/utf8"
)
s := "Hello, 世界"
r, size := utf8.DecodeRune(s) // not DecodeRuneInString 🎉
b := []byte("Hello, 世界")
r, size = utf8.DecodeRune(b) // same API!
func MyFoo[T stringish.Interface](s T) T {
// pass a string or a []byte
// iterate, slice, transform, whatever
}
```
## Motivation
Sometimes we want APIs to accept `string` or `[]byte` without having to convert
between those types. That conversion usually allocates!
By implementing with `stringish.Interface`, we can have a single API, and
single implementation for both types: one `Foo` instead of `Foo` and
`FooString`.
We have converted the
[`unicode/utf8` package](https://github.com/clipperhouse/stringish/blob/main/utf8/utf8.go)
as an example -- note the absence of`*InString` funcs. We might look at `x/text`
next.
## Used by
- clipperhouse/uax29: [stringish trie](https://github.com/clipperhouse/uax29/blob/master/graphemes/trie.go#L27), [stringish iterator](https://github.com/clipperhouse/uax29/blob/master/internal/iterators/iterator.go#L9), [stringish SplitFunc](https://github.com/clipperhouse/uax29/blob/master/graphemes/splitfunc.go#L21)
- [clipperhouse/displaywidth](https://github.com/clipperhouse/displaywidth)
## Prior discussion
- [Consideration of similar by the Go team](https://github.com/golang/go/issues/48643)

View File

@@ -0,0 +1,5 @@
package stringish
type Interface interface {
~[]byte | ~string
}

21
vendor/github.com/clipperhouse/uax29/v2/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Matt Sherman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,82 @@
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode version 15.0.0.
## Quick start
```
go get "github.com/clipperhouse/uax29/v2/graphemes"
```
```go
import "github.com/clipperhouse/uax29/v2/graphemes"
text := "Hello, 世界. Nice dog! 👍🐶"
tokens := graphemes.FromString(text)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
_A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._
## Conformance
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29). Status:
![Go](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
## APIs
### If you have a `string`
```go
text := "Hello, 世界. Nice dog! 👍🐶"
tokens := graphemes.FromString(text)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
### If you have an `io.Reader`
`FromReader` embeds a [`bufio.Scanner`](https://pkg.go.dev/bufio#Scanner), so just use those methods.
```go
r := getYourReader() // from a file or network maybe
tokens := graphemes.FromReader(r)
for tokens.Scan() { // Scan() returns true until error or EOF
fmt.Println(tokens.Text()) // Do something with the current grapheme
}
if tokens.Err() != nil { // Check the error
log.Fatal(tokens.Err())
}
```
### If you have a `[]byte`
```go
b := []byte("Hello, 世界. Nice dog! 👍🐶")
tokens := graphemes.FromBytes(b)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
### Performance
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second. You should see ~constant memory, and no allocations.
### Invalid inputs
Invalid UTF-8 input is considered undefined behavior. We test to ensure that bad inputs will not cause pathological outcomes, such as a panic or infinite loop. Callers should expect “garbage-in, garbage-out”.
Your pipeline should probably include a call to [`utf8.Valid()`](https://pkg.go.dev/unicode/utf8#Valid).

View File

@@ -0,0 +1,28 @@
package graphemes
import "github.com/clipperhouse/uax29/v2/internal/iterators"
type Iterator[T iterators.Stringish] struct {
*iterators.Iterator[T]
}
var (
splitFuncString = splitFunc[string]
splitFuncBytes = splitFunc[[]byte]
)
// FromString returns an iterator for the grapheme clusters in the input string.
// Iterate while Next() is true, and access the grapheme via Value().
func FromString(s string) Iterator[string] {
return Iterator[string]{
iterators.New(splitFuncString, s),
}
}
// FromBytes returns an iterator for the grapheme clusters in the input bytes.
// Iterate while Next() is true, and access the grapheme via Value().
func FromBytes(b []byte) Iterator[[]byte] {
return Iterator[[]byte]{
iterators.New(splitFuncBytes, b),
}
}

View File

@@ -0,0 +1,25 @@
// Package graphemes implements Unicode grapheme cluster boundaries: https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
package graphemes
import (
"bufio"
"io"
)
type Scanner struct {
*bufio.Scanner
}
// FromReader returns a Scanner, to split graphemes per
// https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
//
// It embeds a [bufio.Scanner], so you can use its methods.
//
// Iterate through graphemes by calling Scan() until false, then check Err().
func FromReader(r io.Reader) *Scanner {
sc := bufio.NewScanner(r)
sc.Split(SplitFunc)
return &Scanner{
Scanner: sc,
}
}

View File

@@ -0,0 +1,174 @@
package graphemes
import (
"bufio"
"github.com/clipperhouse/uax29/v2/internal/iterators"
)
// is determines if lookup intersects propert(ies)
func (lookup property) is(properties property) bool {
return (lookup & properties) != 0
}
const _Ignore = _Extend
// SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner.
//
// See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
var SplitFunc bufio.SplitFunc = splitFunc[[]byte]
func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T, err error) {
var empty T
if len(data) == 0 {
return 0, empty, nil
}
// These vars are stateful across loop iterations
var pos int
var lastExIgnore property = 0 // "last excluding ignored categories"
var lastLastExIgnore property = 0 // "last one before that"
var regionalIndicatorCount int
// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
// to the right of the ×, from which we look back or forward
current, w := lookup(data[pos:])
if w == 0 {
if !atEOF {
// Rune extends past current data, request more
return 0, empty, nil
}
pos = len(data)
return pos, data[:pos], nil
}
// https://unicode.org/reports/tr29/#GB1
// Start of text always advances
pos += w
for {
eot := pos == len(data) // "end of text"
if eot {
if !atEOF {
// Token extends past current data, request more
return 0, empty, nil
}
// https://unicode.org/reports/tr29/#GB2
break
}
/*
We've switched the evaluation order of GB1↓ and GB2↑. It's ok:
because we've checked for len(data) at the top of this function,
sot and eot are mutually exclusive, order doesn't matter.
*/
// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
// to the right of the ×, from which we look back or forward
// Remember previous properties to avoid lookups/lookbacks
last := current
if !last.is(_Ignore) {
lastLastExIgnore = lastExIgnore
lastExIgnore = last
}
current, w = lookup(data[pos:])
if w == 0 {
if atEOF {
// Just return the bytes, we can't do anything with them
pos = len(data)
break
}
// Rune extends past current data, request more
return 0, empty, nil
}
// Optimization: no rule can possibly apply
if current|last == 0 { // i.e. both are zero
break
}
// https://unicode.org/reports/tr29/#GB3
if current.is(_LF) && last.is(_CR) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB4
// https://unicode.org/reports/tr29/#GB5
if (current | last).is(_Control | _CR | _LF) {
break
}
// https://unicode.org/reports/tr29/#GB6
if current.is(_L|_V|_LV|_LVT) && last.is(_L) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB7
if current.is(_V|_T) && last.is(_LV|_V) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB8
if current.is(_T) && last.is(_LVT|_T) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9
if current.is(_Extend | _ZWJ) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9a
if current.is(_SpacingMark) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9b
if last.is(_Prepend) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9c
// TODO(clipperhouse):
// It appears to be added in Unicode 15.1.0:
// https://unicode.org/versions/Unicode15.1.0/#Migration
// This package currently supports Unicode 15.0.0, so
// out of scope for now
// https://unicode.org/reports/tr29/#GB11
if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB12
// https://unicode.org/reports/tr29/#GB13
if (current & last).is(_RegionalIndicator) {
regionalIndicatorCount++
odd := regionalIndicatorCount%2 == 1
if odd {
pos += w
continue
}
}
// If we fall through all the above rules, it's a grapheme cluster break
break
}
// Return token
return pos, data[:pos], nil
}

1409
vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go generated vendored Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
package iterators
type Stringish interface {
[]byte | string
}
type SplitFunc[T Stringish] func(T, bool) (int, T, error)
// Iterator is a generic iterator for words that are either []byte or string.
// Iterate while Next() is true, and access the word via Value().
type Iterator[T Stringish] struct {
split SplitFunc[T]
data T
start int
pos int
}
// New creates a new Iterator for the given data and SplitFunc.
func New[T Stringish](split SplitFunc[T], data T) *Iterator[T] {
return &Iterator[T]{
split: split,
data: data,
}
}
// SetText sets the text for the iterator to operate on, and resets all state.
func (iter *Iterator[T]) SetText(data T) {
iter.data = data
iter.start = 0
iter.pos = 0
}
// Split sets the SplitFunc for the Iterator.
func (iter *Iterator[T]) Split(split SplitFunc[T]) {
iter.split = split
}
// Next advances the iterator to the next token. It returns false when there
// are no remaining tokens or an error occurred.
func (iter *Iterator[T]) Next() bool {
if iter.pos == len(iter.data) {
return false
}
if iter.pos > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
iter.start = iter.pos
advance, _, err := iter.split(iter.data[iter.pos:], true)
if err != nil {
panic(err)
}
if advance <= 0 {
panic("SplitFunc returned a zero or negative advance")
}
iter.pos += advance
if iter.pos > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
return true
}
// Value returns the current token.
func (iter *Iterator[T]) Value() T {
return iter.data[iter.start:iter.pos]
}
// Start returns the byte position of the current token in the original data.
func (iter *Iterator[T]) Start() int {
return iter.start
}
// End returns the byte position after the current token in the original data.
func (iter *Iterator[T]) End() int {
return iter.pos
}
// Reset resets the iterator to the beginning of the data.
func (iter *Iterator[T]) Reset() {
iter.start = 0
iter.pos = 0
}

View File

@@ -1,5 +1,24 @@
version: "2"
linters:
disable:
- errcheck
- errcheck
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- gofmt
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -1,6 +1,6 @@
[![Build Status](https://github.com/google/renameio/workflows/Test/badge.svg)](https://github.com/google/renameio/actions?query=workflow%3ATest)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/google/renameio)](https://pkg.go.dev/github.com/google/renameio)
[![Go Report Card](https://goreportcard.com/badge/github.com/google/renameio)](https://goreportcard.com/report/github.com/google/renameio)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/google/renameio/v2)](https://pkg.go.dev/github.com/google/renameio/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/google/renameio/v2)](https://goreportcard.com/report/github.com/google/renameio/v2)
The `renameio` Go package provides a way to atomically create or replace a file or
symbolic link.

View File

@@ -77,3 +77,12 @@ func WithExistingPermissions() Option {
c.attemptPermCopy = true
})
}
// WithReplaceOnClose causes PendingFile.Close() to actually call
// CloseAtomicallyReplace(). This means PendingFile implements io.Closer while
// maintaining atomicity per default.
func WithReplaceOnClose() Option {
return optionFunc(func(c *config) {
c.renameOnClose = true
})
}

View File

@@ -114,9 +114,10 @@ func tempDir(dir, dest string) string {
type PendingFile struct {
*os.File
path string
done bool
closed bool
path string
done bool
closed bool
replaceOnClose bool
}
// Cleanup is a no-op if CloseAtomicallyReplace succeeded, and otherwise closes
@@ -131,7 +132,7 @@ func (t *PendingFile) Cleanup() error {
// reporting, there is nothing the caller can recover here.
var closeErr error
if !t.closed {
closeErr = t.Close()
closeErr = t.File.Close()
}
if err := os.Remove(t.Name()); err != nil {
return err
@@ -159,7 +160,7 @@ func (t *PendingFile) CloseAtomicallyReplace() error {
return err
}
t.closed = true
if err := t.Close(); err != nil {
if err := t.File.Close(); err != nil {
return err
}
if err := os.Rename(t.Name(), t.path); err != nil {
@@ -169,6 +170,15 @@ func (t *PendingFile) CloseAtomicallyReplace() error {
return nil
}
// Close closes the file. By default it just calls Close() on the underlying file. For PendingFiles created with
// WithReplaceOnClose it calls CloseAtomicallyReplace() instead.
func (t *PendingFile) Close() error {
if t.replaceOnClose {
return t.CloseAtomicallyReplace()
}
return t.File.Close()
}
// TempFile creates a temporary file destined to atomically creating or
// replacing the destination file at path.
//
@@ -189,6 +199,7 @@ type config struct {
attemptPermCopy bool
ignoreUmask bool
chmod *os.FileMode
renameOnClose bool
}
// NewPendingFile creates a temporary file destined to atomically creating or
@@ -244,7 +255,7 @@ func NewPendingFile(path string, opts ...Option) (*PendingFile, error) {
}
}
return &PendingFile{File: f, path: cfg.path}, nil
return &PendingFile{File: f, path: cfg.path, replaceOnClose: cfg.renameOnClose}, nil
}
// Symlink wraps os.Symlink, replacing an existing symlink with the same name

43
vendor/github.com/mattn/go-runewidth/benchstat.txt generated vendored Normal file
View File

@@ -0,0 +1,43 @@
goos: darwin
goarch: arm64
pkg: github.com/mattn/go-runewidth
cpu: Apple M2
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
String1WidthAll/regular-8 108.92m ± 0% 35.09m ± 3% -67.78% (p=0.002 n=6)
String1WidthAll/lut-8 93.97m ± 0% 18.70m ± 0% -80.10% (p=0.002 n=6)
String1Width768/regular-8 60.62µ ± 1% 11.54µ ± 0% -80.97% (p=0.002 n=6)
String1Width768/lut-8 60.66µ ± 1% 11.43µ ± 0% -81.16% (p=0.002 n=6)
String1WidthAllEastAsian/regular-8 115.13m ± 1% 40.79m ± 8% -64.57% (p=0.002 n=6)
String1WidthAllEastAsian/lut-8 93.65m ± 0% 18.70m ± 2% -80.03% (p=0.002 n=6)
String1Width768EastAsian/regular-8 75.32µ ± 0% 23.49µ ± 0% -68.82% (p=0.002 n=6)
String1Width768EastAsian/lut-8 60.76µ ± 0% 11.50µ ± 0% -81.07% (p=0.002 n=6)
geomean 2.562m 604.5µ -76.41%
│ old.txt │ new.txt │
│ B/op │ B/op vs base │
String1WidthAll/regular-8 106.3Mi ± 0% 0.0Mi ± 0% -100.00% (p=0.002 n=6)
String1WidthAll/lut-8 106.3Mi ± 0% 0.0Mi ± 0% -100.00% (p=0.002 n=6)
String1Width768/regular-8 75.00Ki ± 0% 0.00Ki ± 0% -100.00% (p=0.002 n=6)
String1Width768/lut-8 75.00Ki ± 0% 0.00Ki ± 0% -100.00% (p=0.002 n=6)
String1WidthAllEastAsian/regular-8 106.3Mi ± 0% 0.0Mi ± 0% -100.00% (p=0.002 n=6)
String1WidthAllEastAsian/lut-8 106.3Mi ± 0% 0.0Mi ± 0% -100.00% (p=0.002 n=6)
String1Width768EastAsian/regular-8 75.00Ki ± 0% 0.00Ki ± 0% -100.00% (p=0.002 n=6)
String1Width768EastAsian/lut-8 75.00Ki ± 0% 0.00Ki ± 0% -100.00% (p=0.002 n=6)
geomean 2.790Mi ? ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean
│ old.txt │ new.txt │
│ allocs/op │ allocs/op vs base │
String1WidthAll/regular-8 3.342M ± 0% 0.000M ± 0% -100.00% (p=0.002 n=6)
String1WidthAll/lut-8 3.342M ± 0% 0.000M ± 0% -100.00% (p=0.002 n=6)
String1Width768/regular-8 2.304k ± 0% 0.000k ± 0% -100.00% (p=0.002 n=6)
String1Width768/lut-8 2.304k ± 0% 0.000k ± 0% -100.00% (p=0.002 n=6)
String1WidthAllEastAsian/regular-8 3.342M ± 0% 0.000M ± 0% -100.00% (p=0.002 n=6)
String1WidthAllEastAsian/lut-8 3.342M ± 0% 0.000M ± 0% -100.00% (p=0.002 n=6)
String1Width768EastAsian/regular-8 2.304k ± 0% 0.000k ± 0% -100.00% (p=0.002 n=6)
String1Width768EastAsian/lut-8 2.304k ± 0% 0.000k ± 0% -100.00% (p=0.002 n=6)
geomean 87.75k ? ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

54
vendor/github.com/mattn/go-runewidth/new.txt generated vendored Normal file
View File

@@ -0,0 +1,54 @@
goos: darwin
goarch: arm64
pkg: github.com/mattn/go-runewidth
cpu: Apple M2
BenchmarkString1WidthAll/regular-8 33 35033923 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 33 34965112 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 33 36307234 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 33 35007705 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 33 35154182 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 34 35155400 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 63 18688500 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 63 18712474 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 63 18700211 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 62 18694179 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 62 18708392 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 63 18770608 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 104137 11526 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 103986 11540 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 104079 11552 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 103963 11530 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 103714 11538 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 104181 11537 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 105150 11420 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 104778 11423 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 105069 11422 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 105127 11475 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 104742 11433 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 105163 11432 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40723347 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40790299 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40801338 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40798216 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 44135253 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40779546 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 18694165 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 18685047 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 18689273 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 19150346 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 63 19126154 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 18712619 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 50775 23595 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51061 23563 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51057 23492 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51138 23445 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51195 23469 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51087 23482 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104559 11549 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104508 11483 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104296 11503 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104606 11485 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104588 11495 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104602 11518 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/mattn/go-runewidth 64.455s

54
vendor/github.com/mattn/go-runewidth/old.txt generated vendored Normal file
View File

@@ -0,0 +1,54 @@
goos: darwin
goarch: arm64
pkg: github.com/mattn/go-runewidth
cpu: Apple M2
BenchmarkString1WidthAll/regular-8 10 108559258 ns/op 111412145 B/op 3342342 allocs/op
BenchmarkString1WidthAll/regular-8 10 108968079 ns/op 111412364 B/op 3342343 allocs/op
BenchmarkString1WidthAll/regular-8 10 108890338 ns/op 111412388 B/op 3342344 allocs/op
BenchmarkString1WidthAll/regular-8 10 108940704 ns/op 111412584 B/op 3342346 allocs/op
BenchmarkString1WidthAll/regular-8 10 108632796 ns/op 111412348 B/op 3342343 allocs/op
BenchmarkString1WidthAll/regular-8 10 109354546 ns/op 111412777 B/op 3342343 allocs/op
BenchmarkString1WidthAll/lut-8 12 93844406 ns/op 111412569 B/op 3342345 allocs/op
BenchmarkString1WidthAll/lut-8 12 93991080 ns/op 111412512 B/op 3342344 allocs/op
BenchmarkString1WidthAll/lut-8 12 93980632 ns/op 111412413 B/op 3342343 allocs/op
BenchmarkString1WidthAll/lut-8 12 94004083 ns/op 111412396 B/op 3342343 allocs/op
BenchmarkString1WidthAll/lut-8 12 93959795 ns/op 111412445 B/op 3342343 allocs/op
BenchmarkString1WidthAll/lut-8 12 93846198 ns/op 111412556 B/op 3342345 allocs/op
BenchmarkString1Width768/regular-8 19785 60696 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19824 60520 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19832 60547 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19778 60543 ns/op 76800 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19842 61142 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19780 60696 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19598 61161 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19731 60707 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19738 60626 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19764 60670 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19797 60642 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19738 60608 ns/op 76800 B/op 2304 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115080431 ns/op 111412458 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 114908880 ns/op 111412476 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115077134 ns/op 111412540 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115175292 ns/op 111412467 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115792653 ns/op 111412362 B/op 3342344 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115255417 ns/op 111412572 B/op 3342346 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93761542 ns/op 111412538 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 94089990 ns/op 111412440 B/op 3342343 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93721410 ns/op 111412514 B/op 3342344 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93572951 ns/op 111412329 B/op 3342342 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93536052 ns/op 111412206 B/op 3342341 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93532365 ns/op 111412412 B/op 3342343 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15904 75401 ns/op 76800 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15932 75449 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15944 75181 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15963 75311 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15879 75292 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15955 75334 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19692 60692 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19712 60699 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19741 60819 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19771 60653 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19737 61027 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19657 60820 ns/op 76801 B/op 2304 allocs/op
PASS
ok github.com/mattn/go-runewidth 76.165s

View File

@@ -4,7 +4,7 @@ import (
"os"
"strings"
"github.com/rivo/uniseg"
"github.com/clipperhouse/uax29/v2/graphemes"
)
//go:generate go run script/generate.go
@@ -64,6 +64,9 @@ func inTable(r rune, t table) bool {
if r < t[0].first {
return false
}
if r > t[len(t)-1].last {
return false
}
bot := 0
top := len(t) - 1
@@ -175,10 +178,10 @@ func (c *Condition) CreateLUT() {
// StringWidth return width as you can see
func (c *Condition) StringWidth(s string) (width int) {
g := uniseg.NewGraphemes(s)
g := graphemes.FromString(s)
for g.Next() {
var chWidth int
for _, r := range g.Runes() {
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // Our best guess at this point is to use the width of the first non-zero-width rune.
@@ -197,17 +200,17 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
w -= c.StringWidth(tail)
var width int
pos := len(s)
g := uniseg.NewGraphemes(s)
g := graphemes.FromString(s)
for g.Next() {
var chWidth int
for _, r := range g.Runes() {
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
}
}
if width+chWidth > w {
pos, _ = g.Positions()
pos = g.Start()
break
}
width += chWidth
@@ -224,10 +227,10 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
var width int
pos := len(s)
g := uniseg.NewGraphemes(s)
g := graphemes.FromString(s)
for g.Next() {
var chWidth int
for _, r := range g.Runes() {
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
@@ -236,10 +239,10 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
if width+chWidth > w {
if width < w {
_, pos = g.Positions()
pos = g.End()
prefix += strings.Repeat(" ", width+chWidth-w)
} else {
pos, _ = g.Positions()
pos = g.Start()
}
break

View File

@@ -4,6 +4,7 @@
package runewidth
import (
"os"
"syscall"
)
@@ -14,6 +15,11 @@ var (
// IsEastAsian return true if the current locale is CJK
func IsEastAsian() bool {
if os.Getenv("WT_SESSION") != "" {
// Windows Terminal always not use East Asian Ambiguous Width(s).
return false
}
r1, _, _ := procGetConsoleOutputCP.Call()
if r1 == 0 {
return false

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