Merge branch 'master' into toggle-unified-roles

This commit is contained in:
Florian Schade
2024-08-27 17:43:05 +02:00
232 changed files with 2859 additions and 4916 deletions
+27 -1
View File
@@ -1,4 +1,4 @@
# Activitylog Service
# Activitylog
The `activitylog` service is responsible for storing events (activities) per resource.
@@ -13,3 +13,29 @@ Log services like the `activitylog`, `userlog`, `clientlog` and `sse` are respon
## Activitylog Store
The `activitylog` stores activities for each resource. It works in conjunction with the `eventhistory` service to keep the data it needs to store to a minimum.
## Translations
The `activitylog` service has embedded translations sourced via transifex to provide a basic set of translated languages. These embedded translations are available for all deployment scenarios. In addition, the service supports custom translations, though it is currently not possible to just add custom translations to embedded ones. If custom translations are configured, the embedded ones are not used. To configure custom translations, the `ACTIVITYLOG_TRANSLATION_PATH` environment variable needs to point to a base folder that will contain the translation files. This path must be available from all instances of the activitylog service, a shared storage is recommended. Translation files must be of type [.po](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files) or [.mo](https://www.gnu.org/software/gettext/manual/html_node/Binaries.html). For each language, the filename needs to be `activitylog.po` (or `activitylog.mo`) and stored in a folder structure defining the language code. In general the path/name pattern for a translation file needs to be:
```text
{ACTIVITYLOG_TRANSLATION_PATH}/{language-code}/LC_MESSAGES/activitylog.po
```
The language code pattern is composed of `language[_territory]` where `language` is the base language and `_territory` is optional and defines a country.
For example, for the language `de`, one needs to place the corresponding translation files to `{ACTIVITYLOG_TRANSLATION_PATH}/de_DE/LC_MESSAGES/activitylog.po`.
<!-- also see the notifications readme -->
Important: For the time being, the embedded ownCloud Web frontend only supports the main language code but does not handle any territory. When strings are available in the language code `language_territory`, the web frontend does not see it as it only requests `language`. In consequence, any translations made must exist in the requested `language` to avoid a fallback to the default.
### Translation Rules
* If a requested language code is not available, the service tries to fall back to the base language if available. For example, if the requested language-code `de_DE` is not available, the service tries to fall back to translations in the `de` folder.
* If the base language `de` is also not available, the service falls back to the system's default English (`en`),
which is the source of the texts provided by the code.
## Default Language
The default language can be defined via the `OCIS_DEFAULT_LANGUAGE` environment variable. See the `settings` service for a detailed description.
@@ -26,6 +26,9 @@ type Config struct {
HTTP HTTP `yaml:"http"`
TokenManager *TokenManager `yaml:"token_manager"`
TranslationPath string `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH;ACTIVITYLOG_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details." introductionVersion:"%%NEXT%%"`
DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details." introductionVersion:"%%NEXT%%"`
ServiceAccount ServiceAccount `yaml:"service_account"`
Context context.Context `yaml:"-"`
@@ -37,7 +37,8 @@ func DefaultConfig() *config.Config {
Database: "activitylog",
Table: "",
},
RevaGateway: shared.DefaultRevaConfig().Address,
RevaGateway: shared.DefaultRevaConfig().Address,
DefaultLanguage: "en",
HTTP: config.HTTP{
Addr: "127.0.0.1:0",
Root: "/",
+1 -2
View File
@@ -164,9 +164,8 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h
continue
}
// FIXME: configurable default locale?
loc := l10n.MustGetUserLocale(r.Context(), activeUser.GetId().GetOpaqueId(), r.Header.Get(l10n.HeaderAcceptLanguage), s.valService)
t := l10n.NewTranslatorFromCommonConfig("en", _domain, "", _localeFS, _localeSubPath)
t := l10n.NewTranslatorFromCommonConfig(s.cfg.DefaultLanguage, _domain, s.cfg.TranslationPath, _localeFS, _localeSubPath)
resp.Activities = append(resp.Activities, NewActivity(t.Translate(message, loc), ts, e.GetId(), vars))
}
@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2024-08-21 05:51+0000\n"
"POT-Creation-Date: 2024-08-22 00:53+0000\n"
"PO-Revision-Date: 2024-08-20 10:13+0000\n"
"Last-Translator: Martin <github@diemattels.at>, 2024\n"
"Language-Team: German (https://app.transifex.com/owncloud-org/teams/6149/de/)\n"
@@ -23,15 +23,15 @@ msgstr ""
#: pkg/service/response.go:23
msgid "{user} added {resource} to {space}"
msgstr "{user} hat {resource} zum {space} hinzugefügt"
msgstr "{user} hat {resource} zu {space} hinzugefügt"
#: pkg/service/response.go:31
msgid "{user} added {sharee} as member of {space}"
msgstr "{user} hat {sharee} als Mitglied zum {space} hinzugefügt"
msgstr "{user} hat {sharee} als Mitglied zu {space} hinzugefügt"
#: pkg/service/response.go:24
msgid "{user} deleted {resource} from {space}"
msgstr "{user} hat {resource} vom {space} gelöscht"
msgstr "{user} hat {resource} in {space} gelöscht"
#: pkg/service/response.go:25
msgid "{user} moved {resource} to {space}"
@@ -0,0 +1,62 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Viktor Scharf, 2024
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2024-08-24 00:53+0000\n"
"PO-Revision-Date: 2024-08-20 10:13+0000\n"
"Last-Translator: Viktor Scharf, 2024\n"
"Language-Team: Russian (https://app.transifex.com/owncloud-org/teams/6149/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ru\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
#: pkg/service/response.go:23
msgid "{user} added {resource} to {space}"
msgstr "{user} добавил {resource} в {space}"
#: pkg/service/response.go:31
msgid "{user} added {sharee} as member of {space}"
msgstr "{user} добавил {sharee} как участника {space}"
#: pkg/service/response.go:24
msgid "{user} deleted {resource} from {space}"
msgstr "{user} удалил {resource} из {space}"
#: pkg/service/response.go:25
msgid "{user} moved {resource} to {space}"
msgstr "{user} переместил {resource} в {space}"
#: pkg/service/response.go:30
msgid "{user} removed link to {resource}"
msgstr "{user} удалил ссылку на {resource}"
#: pkg/service/response.go:28
msgid "{user} removed {sharee} from {resource}"
msgstr "{user} удалил {sharee} из {resource}"
#: pkg/service/response.go:32
msgid "{user} removed {sharee} from {space}"
msgstr "{user} удалил {sharee} из {space}"
#: pkg/service/response.go:26
msgid "{user} renamed {oldResource} to {resource}"
msgstr "{user} переименовал {oldResource} в {resource}"
#: pkg/service/response.go:29
msgid "{user} shared {resource} via link"
msgstr "{user} поделился {resource} через ссылку"
#: pkg/service/response.go:27
msgid "{user} shared {resource} with {sharee}"
msgstr "{user} поделился {resource} с {sharee}"
@@ -0,0 +1,62 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Besnik Bleta <besnik@programeshqip.org>, 2024
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2024-08-22 00:53+0000\n"
"PO-Revision-Date: 2024-08-20 10:13+0000\n"
"Last-Translator: Besnik Bleta <besnik@programeshqip.org>, 2024\n"
"Language-Team: Albanian (https://app.transifex.com/owncloud-org/teams/6149/sq/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: sq\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: pkg/service/response.go:23
msgid "{user} added {resource} to {space}"
msgstr "{user} shtoi {resource} te {space}"
#: pkg/service/response.go:31
msgid "{user} added {sharee} as member of {space}"
msgstr "{user} shtoi {sharee} si anëtar të {space}"
#: pkg/service/response.go:24
msgid "{user} deleted {resource} from {space}"
msgstr "{user} fshiu {resource} nga {space}"
#: pkg/service/response.go:25
msgid "{user} moved {resource} to {space}"
msgstr "{user} e shpuri {resource} te {space}"
#: pkg/service/response.go:30
msgid "{user} removed link to {resource}"
msgstr "{user} hoqi lidhjen për te {resource}"
#: pkg/service/response.go:28
msgid "{user} removed {sharee} from {resource}"
msgstr "{user} hoqi {sharee} nga {resource}"
#: pkg/service/response.go:32
msgid "{user} removed {sharee} from {space}"
msgstr "{user} hoqi {sharee} nga {space}"
#: pkg/service/response.go:26
msgid "{user} renamed {oldResource} to {resource}"
msgstr "{user} riemërtoi {oldResource} si {resource}"
#: pkg/service/response.go:29
msgid "{user} shared {resource} via link"
msgstr "{user} ndau me të tjerët {resource} përmes lidhjeje"
#: pkg/service/response.go:27
msgid "{user} shared {resource} with {sharee}"
msgstr "{user} ndau {resource} me {sharee}"
@@ -99,6 +99,7 @@ func WithTrashedResource(ref *provider.Reference, rid *provider.ResourceId) Acti
resp, err := gwc.ListRecycle(ctx, &provider.ListRecycleRequest{
Ref: ref,
Key: rid.GetOpaqueId(),
})
if err != nil {
return err
+1 -1
View File
@@ -81,7 +81,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -48,7 +48,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"APP_PROVIDER_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"APP_PROVIDER_GRPC_PROTOCOL" desc:"The transport protocol of the GPRC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;APP_PROVIDER_GRPC_PROTOCOL" desc:"The transport protocol of the GPRC service." introductionVersion:"pre5.0"`
}
type Drivers struct {
+1 -1
View File
@@ -76,7 +76,7 @@ func Server(cfg *config.Config) *cli.Command {
cancel()
})
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -47,7 +47,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"APP_REGISTRY_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"APP_REGISTRY_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;APP_REGISTRY_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
}
type AppRegistry struct {
+1 -1
View File
@@ -89,7 +89,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -58,7 +58,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"AUTH_APP_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"%%NEXT%%"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"AUTH_APP_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"%%NEXT%%"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;AUTH_APP_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"%%NEXT%%"`
}
// HTTP defines the available http configuration.
+1 -1
View File
@@ -94,7 +94,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -48,7 +48,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"AUTH_BASIC_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"AUTH_BASIC_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;AUTH_BASIC_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
}
type AuthProviders struct {
+1 -1
View File
@@ -81,7 +81,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -48,7 +48,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"AUTH_BEARER_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"AUTH_BEARER_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;AUTH_BEARER_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
}
type OIDC struct {
+1 -1
View File
@@ -81,7 +81,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -48,5 +48,5 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"AUTH_MACHINE_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"AUTH_MACHINE_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;AUTH_MACHINE_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
}
+1 -1
View File
@@ -82,7 +82,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -47,7 +47,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"AUTH_SERVICE_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"AUTH_SERVICE_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;AUTH_SERVICE_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"5.0"`
}
// ServiceAccount is the configuration for the used service account
@@ -221,6 +221,7 @@ func processShareEvent(ctx context.Context, ref *provider.Reference, gwc gateway
func processItemTrashedEvent(ctx context.Context, ref *provider.Reference, gwc gateway.GatewayAPIClient, initiatorid string, itemID *provider.ResourceId) ([]string, FileEvent, error) {
resp, err := gwc.ListRecycle(ctx, &provider.ListRecycleRequest{
Ref: ref,
Key: itemID.GetOpaqueId(),
})
if err != nil {
return nil, FileEvent{}, err
@@ -33,6 +33,7 @@ func DefaultConfig() *config.Config {
},
GRPC: config.GRPC{
Addr: "127.0.0.1:9301",
Protocol: "tcp",
Namespace: "com.owncloud.api",
},
HTTP: config.HTTP{
+3 -1
View File
@@ -2,6 +2,8 @@ package config
// GRPC defines the available grpc configuration.
type GRPC struct {
Addr string `yaml:"addr" env:"COLLABORATION_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"6.0.0"`
Addr string `yaml:"addr" env:"COLLABORATION_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"6.0.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;COLLABORATION_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"%%NEXT%%"`
Namespace string `yaml:"-"`
}
@@ -52,10 +52,8 @@ var _ = Describe("ContentConnector", func() {
},
Path: ".",
},
User: &userv1beta1.User{}, // Not used for now
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
EditAppUrl: "http://test.ex.prv/edit",
ViewAppUrl: "http://test.ex.prv/view",
User: &userv1beta1.User{}, // Not used for now
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
}
randomContent = "This is the content of the test.txt file"
@@ -186,10 +184,8 @@ var _ = Describe("ContentConnector", func() {
},
Path: ".",
},
User: &userv1beta1.User{}, // Not used for now
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY,
EditAppUrl: "http://test.ex.prv/edit",
ViewAppUrl: "http://test.ex.prv/view",
User: &userv1beta1.User{}, // Not used for now
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY,
}
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
@@ -1050,8 +1050,9 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
// to get the folder we actually need to do a GetPath() request
//BreadcrumbFolderName: path.Dir(statRes.Info.Path),
fileinfo.KeyHostViewURL: wopiContext.ViewAppUrl,
fileinfo.KeyHostEditURL: wopiContext.EditAppUrl,
// TODO: these URLs must point to ocis, which is hosting the editor's iframe
//fileinfo.KeyHostViewURL: wopiContext.ViewAppUrl,
//fileinfo.KeyHostEditURL: wopiContext.EditAppUrl,
fileinfo.KeyEnableOwnerTermination: true, // only for collabora
fileinfo.KeySupportsExtendedLockLength: true,
@@ -1061,7 +1062,6 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
fileinfo.KeySupportsDeleteFile: true,
fileinfo.KeySupportsRename: true,
//fileinfo.KeyUserCanNotWriteRelative: true,
fileinfo.KeyIsAnonymousUser: isAnonymousUser,
fileinfo.KeyUserFriendlyName: userFriendlyName,
fileinfo.KeyUserID: userId,
@@ -82,9 +82,7 @@ var _ = Describe("FileConnector", func() {
// },
//},
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
EditAppUrl: "http://test.ex.prv/edit",
ViewAppUrl: "http://test.ex.prv/view",
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
}
})
@@ -1546,8 +1544,6 @@ var _ = Describe("FileConnector", func() {
BaseFileName: "test.txt",
BreadcrumbDocName: "test.txt",
UserCanNotWriteRelative: false,
HostViewURL: "http://test.ex.prv/view",
HostEditURL: "http://test.ex.prv/edit",
SupportsExtendedLockLength: true,
SupportsGetLock: true,
SupportsLocks: true,
@@ -18,7 +18,7 @@ import (
// There are no explicit requirements for the context, and it will be passed
// without changes to the underlying RegisterService method.
func RegisterOcisService(ctx context.Context, cfg *config.Config, logger log.Logger) error {
svc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name+"."+cfg.App.Name, cfg.GRPC.Addr, version.GetString())
svc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name+"."+cfg.App.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
return registry.RegisterService(ctx, svc, logger)
}
@@ -1,6 +1,6 @@
package middleware
import "github.com/golang-jwt/jwt/v4"
import "github.com/golang-jwt/jwt/v5"
// Claims contains the jwt registered claims plus the used WOPI context
type Claims struct {
@@ -11,7 +11,7 @@ import (
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
"github.com/rs/zerolog"
@@ -31,8 +31,6 @@ type WopiContext struct {
FileReference *providerv1beta1.Reference
User *userv1beta1.User
ViewMode appproviderv1beta1.ViewMode
EditAppUrl string
ViewAppUrl string
}
// WopiContextAuthMiddleware will prepare an HTTP handler to be used as
@@ -71,11 +69,6 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl
return
}
if err := claims.Valid(); err != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
ctx := r.Context()
wopiContextAccessToken, err := DecryptAES([]byte(cfg.Wopi.Secret), claims.WopiContext.AccessToken)
@@ -2,10 +2,11 @@ package service
import (
"context"
"fmt"
"errors"
"net/url"
"path"
"strconv"
"strings"
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
@@ -80,147 +81,47 @@ func (s *Service) OpenInApp(
Path: ".",
}
// build a urlsafe and stable file reference that can be used for proxy routing,
// so that all sessions on one file end on the same office server
fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId())
logger := s.logger.With().
Str("FileReference", providerFileRef.String()).
Str("ViewMode", req.GetViewMode().String()).
Str("Requester", user.GetId().String()).
Logger()
// get the file extension to use the right wopi app url
fileExt := path.Ext(req.GetResourceInfo().GetPath())
var viewCommentAppURL string
var viewAppURL string
var editAppURL string
if viewCommentAppURLs, ok := s.appURLs["view_comment"]; ok {
if u, ok := viewCommentAppURLs[fileExt]; ok {
viewCommentAppURL = u
}
}
if viewAppURLs, ok := s.appURLs["view"]; ok {
if u, ok := viewAppURLs[fileExt]; ok {
viewAppURL = u
}
}
if editAppURLs, ok := s.appURLs["edit"]; ok {
if u, ok := editAppURLs[fileExt]; ok {
editAppURL = u
}
}
if editAppURL == "" && viewAppURL == "" && viewCommentAppURL == "" {
err := fmt.Errorf("OpenInApp: neither edit nor view app url found")
s.logger.Error().
Err(err).
Str("FileReference", providerFileRef.String()).
Str("ViewMode", req.GetViewMode().String()).
Str("Requester", user.GetId().String()).Send()
return nil, err
// get the appURL we need to use
appURL := s.getAppUrl(fileExt, req.GetViewMode())
if appURL == "" {
logger.Error().Msg("OpenInApp: neither edit nor view app URL found")
return nil, errors.New("neither edit nor view app URL found")
}
if editAppURL == "" {
// assuming that an view action is always available in the /hosting/discovery manifest
// eg. Collabora does support viewing jpgs but no editing
// eg. OnlyOffice does support viewing pdfs but no editing
// there is no known case of supporting edit only without view
editAppURL = viewAppURL
}
if viewAppURL == "" {
// the URL of the end-user application in view mode when different (defaults to edit mod URL)
viewAppURL = editAppURL
}
// TODO: check if collabora will support an "edit" url in the future
if viewAppURL == "" && editAppURL == "" && viewCommentAppURL != "" {
// there are rare cases where neither view nor edit is supported but view_comment is
viewAppURL = viewCommentAppURL
// that can be the case for editable and viewable files
if req.GetViewMode() == appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE {
editAppURL = viewCommentAppURL
}
}
wopiSrcURL, err := url.Parse(s.config.Wopi.WopiSrc)
// append the parameters we need
appURL, err = s.addQueryToURL(appURL, req)
if err != nil {
return nil, err
}
wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
addWopiSrcQueryParam := func(baseURL string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
q := u.Query()
q.Add("WOPISrc", wopiSrcURL.String())
if s.config.Wopi.DisableChat {
q.Add("dchat", "1")
}
lang := utils.ReadPlainFromOpaque(req.GetOpaque(), "lang")
if lang != "" {
q.Add("ui", lang) // OnlyOffice
q.Add("lang", lang) // Collabora, Impact on the default document language of OnlyOffice
q.Add("UI_LLCC", lang) // Office365
}
qs := q.Encode()
u.RawQuery = qs
return u.String(), nil
}
viewAppURL, err = addWopiSrcQueryParam(viewAppURL)
if err != nil {
s.logger.Error().
Err(err).
Str("FileReference", providerFileRef.String()).
Str("ViewMode", req.GetViewMode().String()).
Str("Requester", user.GetId().String()).
Msg("OpenInApp: error parsing viewAppUrl")
return nil, err
}
editAppURL, err = addWopiSrcQueryParam(editAppURL)
if err != nil {
s.logger.Error().
Err(err).
Str("FileReference", providerFileRef.String()).
Str("ViewMode", req.GetViewMode().String()).
Str("Requester", user.GetId().String()).
Msg("OpenInApp: error parsing editAppUrl")
logger.Error().Err(err).Msg("OpenInApp: error parsing appUrl")
return nil, err
}
appURL := viewAppURL
if req.GetViewMode() == appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE {
appURL = editAppURL
}
// create the wopiContext and generate the token
wopiContext := middleware.WopiContext{
AccessToken: req.GetAccessToken(), // it will be encrypted
ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"),
FileReference: &providerFileRef,
User: user,
ViewMode: req.GetViewMode(),
EditAppUrl: editAppURL,
ViewAppUrl: viewAppURL,
}
accessToken, accessExpiration, err := middleware.GenerateWopiToken(wopiContext, s.config)
if err != nil {
s.logger.Error().
Err(err).
Str("FileReference", providerFileRef.String()).
Str("ViewMode", req.GetViewMode().String()).
Str("Requester", user.GetId().String()).
Msg("OpenInApp: error generating the token")
logger.Error().Err(err).Msg("OpenInApp: error generating the token")
return &appproviderv1beta1.OpenInAppResponse{
Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_INTERNAL},
}, err
}
s.logger.Debug().
Str("FileReference", providerFileRef.String()).
Str("ViewMode", req.GetViewMode().String()).
Str("Requester", user.GetId().String()).
Msg("OpenInApp: success")
logger.Debug().Msg("OpenInApp: success")
return &appproviderv1beta1.OpenInAppResponse{
Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_OK},
@@ -237,3 +138,97 @@ func (s *Service) OpenInApp(
},
}, nil
}
// getAppUrlFor gets the appURL from the list of appURLs based on the
// action and file extension provided. If there is no match, an empty
// string will be returned.
func (s *Service) getAppUrlFor(action, fileExt string) string {
if actionURL, ok := s.appURLs[action]; ok {
if actionExtensionURL, ok := actionURL[fileExt]; ok {
return actionExtensionURL
}
}
return ""
}
// getAppUrl will get the appURL that should be used based on the extension
// and the provided view mode.
// "view" urls will be chosen first, then if the view mode is "read/write",
// "edit" urls will be prioritized. Note that "view" url might be returned for
// "read/write" view mode if no "edit" url is found.
func (s *Service) getAppUrl(fileExt string, viewMode appproviderv1beta1.ViewMode) string {
// prioritize view action if possible
appURL := s.getAppUrlFor("view", fileExt)
if strings.ToLower(s.config.App.Name) == "collabora" {
// collabora provides only one action per extension. usual options
// are "view" (checked above), "edit" or "view_comment" (this last one
// is exclusive of collabora)
if appURL == "" {
if editURL := s.getAppUrlFor("edit", fileExt); editURL != "" {
return editURL
}
if commentURL := s.getAppUrlFor("view_comment", fileExt); commentURL != "" {
return commentURL
}
}
} else {
// If not collabora, there might be an edit action for the extension.
// If read/write mode has been requested, prioritize edit action.
if viewMode == appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE {
if editAppURL := s.getAppUrlFor("edit", fileExt); editAppURL != "" {
appURL = editAppURL
}
}
}
return appURL
}
// addQueryToURL will add specific query parameters to the baseURL. These
// parameters are:
// * "WOPISrc" pointing to the requested resource in the OpenInAppRequest
// * "dchat" to disable the chat, based on configuration
// * "lang" (WOPI app dependent) with the language in the request. "lang"
// for collabora, "ui" for onlyoffice and "UI_LLCC" for the rest
func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAppRequest) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
// build a urlsafe and stable file reference that can be used for proxy routing,
// so that all sessions on one file end on the same office server
fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId())
wopiSrcURL, err := url.Parse(s.config.Wopi.WopiSrc)
if err != nil {
return "", err
}
wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
q := u.Query()
q.Add("WOPISrc", wopiSrcURL.String())
if s.config.Wopi.DisableChat {
q.Add("dchat", "1")
}
lang := utils.ReadPlainFromOpaque(req.GetOpaque(), "lang")
if lang != "" {
switch strings.ToLower(s.config.App.Name) {
case "collabora":
q.Add("lang", lang)
case "onlyoffice":
q.Add("ui", lang)
default:
q.Add("UI_LLCC", lang)
}
}
qs := q.Encode()
u.RawQuery = qs
return u.String(), nil
}
@@ -6,7 +6,7 @@ import (
"time"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
@@ -136,133 +136,66 @@ var _ = Describe("Discovery", func() {
Expect(resp).To(BeNil())
})
It("Success", func() {
ctx := context.Background()
nowTime := time.Now()
DescribeTable(
"Success",
func(appName, lang string, disableChat bool, expectedAppUrl string) {
ctx := context.Background()
nowTime := time.Now()
cfg.Wopi.WopiSrc = "https://wopiserver.test.prv"
cfg.Wopi.Secret = "my_supa_secret"
cfg.Wopi.WopiSrc = "https://wopiserver.test.prv"
cfg.Wopi.Secret = "my_supa_secret"
cfg.Wopi.DisableChat = disableChat
cfg.App.Name = appName
myself := &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Username: "username",
}
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
myself := &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Path: "/path/to/file.docx",
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
}
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "de")
Username: "username",
}
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: myself,
}, nil)
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(Succeed())
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK))
Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST"))
Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&lang=de&ui=de"))
Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
})
It("Success", func() {
ctx := context.Background()
nowTime := time.Now()
cfg.Wopi.WopiSrc = "https://wopiserver.test.prv"
cfg.Wopi.Secret = "my_supa_secret"
cfg.Wopi.DisableChat = true
myself := &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Username: "username",
}
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
},
Path: "/path/to/file.docx",
},
Path: "/path/to/file.docx",
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
}
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
}
if lang != "" {
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", lang)
}
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: myself,
}, nil)
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: myself,
}, nil)
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(Succeed())
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK))
Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST"))
Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"))
Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
})
It("Success", func() {
ctx := context.Background()
nowTime := time.Now()
cfg.Wopi.WopiSrc = "https://wopiserver.test.prv"
cfg.Wopi.Secret = "my_supa_secret"
cfg.Wopi.DisableChat = true
myself := &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Username: "username",
}
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
},
Path: "/path/to/file.docx",
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
}
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: myself,
}, nil)
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(Succeed())
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK))
Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST"))
Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"))
Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
})
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(Succeed())
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK))
Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST"))
Expect(resp.GetAppUrl().GetAppUrl()).To(Equal(expectedAppUrl))
Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
},
Entry("Microsoft chat no lang", "Microsoft", "", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
Entry("Collabora chat no lang", "Collabora", "", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
Entry("OnlyOffice chat no lang", "OnlyOffice", "", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
Entry("Microsoft chat lang", "Microsoft", "de", false, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
Entry("Collabora chat lang", "Collabora", "de", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&lang=de"),
Entry("OnlyOffice chat lang", "OnlyOffice", "de", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&ui=de"),
Entry("Microsoft no chat no lang", "Microsoft", "", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
Entry("Collabora no chat no lang", "Collabora", "", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
Entry("OnlyOffice no chat no lang", "OnlyOffice", "", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
Entry("Microsoft no chat lang", "Microsoft", "de", true, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
Entry("Collabora no chat lang", "Collabora", "de", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&lang=de"),
Entry("OnlyOffice no chat lang", "OnlyOffice", "de", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&ui=de"),
)
})
})
+69 -2
View File
@@ -31,7 +31,74 @@ Store specific notes:
- When using `nats-js-kv` it is recommended to set `OCIS_CACHE_STORE_NODES` to the same value as `OCIS_EVENTS_ENDPOINT`. That way the cache uses the same nats instance as the event bus.
- When using the `nats-js-kv` store, it is possible to set `OCIS_CACHE_DISABLE_PERSISTENCE` to instruct nats to not persist cache data on disc.
## Storage registry
## Service Endpoints
**IMPORTANT**\
This functionality is currently experimental.
The gateway acts as a proxy for other CS3 services. As such it has to forward requests to a lot of services and needs to establish connections by looking up the IP address using the service registry. Instead of using the service registry each endpoint can also be configured to use the grpc `dns://` or `kubernetes://` URLs, which might be useful when running in kubernetes.
For a local single node deployment you might want to use `unix:` sockets as shown below. Using unix sockets will reduce the amount of service lookups and omit the TCP stack. For now, this is experimental and the services do not delete the socket on shutdown. PRs welcome.
```console
USERS_GRPC_PROTOCOL=unix"
USERS_GRPC_ADDR=/var/run/ocis/users.sock"
GATEWAY_USERS_ENDPOINT=unix:/var/run/ocis/users.sock"
GROUPS_GRPC_PROTOCOL=unix"
GROUPS_GRPC_ADDR=/var/run/ocis/groups.sock"
GATEWAY_GROUPS_ENDPOINT=unix:/var/run/ocis/groups.sock"
AUTH_APP_GRPC_PROTOCOL=unix"
AUTH_APP_GRPC_ADDR=/var/run/ocis/auth-app.sock"
GATEWAY_AUTH_APP_ENDPOINT=unix:/var/run/ocis/auth-app.sock"
AUTH_BASIC_GRPC_PROTOCOL=unix"
AUTH_BASIC_GRPC_ADDR=/var/run/ocis/auth-basic.sock"
GATEWAY_AUTH_BASIC_ENDPOINT=unix:/var/run/ocis/auth-basic.sock"
AUTH_MACHINE_GRPC_PROTOCOL=unix"
AUTH_MACHINE_GRPC_ADDR=/var/run/ocis/auth-machine.sock"
GATEWAY_AUTH_MACHINE_ENDPOINT=unix:/var/run/ocis/auth-machine.sock"
AUTH_SERVICE_GRPC_PROTOCOL=unix"
AUTH_SERVICE_GRPC_ADDR=/var/run/ocis/auth-service.sock"
GATEWAY_AUTH_SERVICE_ENDPOINT=unix:/var/run/ocis/auth-service.sock"
STORAGE_PUBLIC_LINK_GRPC_PROTOCOL=unix"
STORAGE_PUBLIC_LINK_GRPC_ADDR=/var/run/ocis/storage-public-link.sock"
GATEWAY_STORAGE_PUBLIC_LINK_ENDPOINT=unix:/var/run/ocis/storage-public-link.sock"
STORAGE_USERS_GRPC_PROTOCOL=unix"
STORAGE_USERS_GRPC_ADDR=/var/run/ocis/storage-users.sock"
GATEWAY_STORAGE_USERS_ENDPOINT=unix:/var/run/ocis/storage-users.sock"
// graph sometimes bypasses the gateway so we need to configure the socket here as wel
GRAPH_SPACES_STORAGE_USERS_ADDRESS=unix:/var/run/ocis/storage-users.sock"
STORAGE_SHARES_GRPC_PROTOCOL=unix"
STORAGE_SHARES_GRPC_ADDR=/var/run/ocis/storage-shares.sock"
GATEWAY_STORAGE_SHARES_ENDPOINT=unix:/var/run/ocis/storage-shares.sock"
APP_REGISTRY_GRPC_PROTOCOL=unix"
APP_REGISTRY_GRPC_ADDR=/var/run/ocis/app-registry.sock"
GATEWAY_APP_REGISTRY_ENDPOINT=unix:/var/run/ocis/app-registry.sock"
OCM_GRPC_PROTOCOL=unix"
OCM_GRPC_ADDR=/var/run/ocis/ocm.sock"
GATEWAY_OCM_ENDPOINT=unix:/var/run/ocis/ocm.sock"
// storage system
STORAGE_SYSTEM_GRPC_PROTOCOL="unix"
STORAGE_SYSTEM_GRPC_ADDR="/var/run/ocis/storage-system.sock"
STORAGE_GATEWAY_GRPC_ADDR="unix:/var/run/ocis/storage-system.sock"
STORAGE_GRPC_ADDR="unix:/var/run/ocis/storage-system.sock"
SHARING_USER_CS3_PROVIDER_ADDR="unix:/var/run/ocis/storage-system.sock"
SHARING_USER_JSONCS3_PROVIDER_ADDR="unix:/var/run/ocis/storage-system.sock"
SHARING_PUBLIC_CS3_PROVIDER_ADDR="unix:/var/run/ocis/storage-system.sock"
SHARING_PUBLIC_JSONCS3_PROVIDER_ADDR="unix:/var/run/ocis/storage-system.sock"
```
## Storage Registry
In order to add another storage provider the CS3 storage registry that is running as part of the CS3 gateway hes to be made aware of it. The easiest cleanest way to do it is to set `GATEWAY_STORAGE_REGISTRY_CONFIG_JSON=/path/to/storages.json` and list all storage providers like this:
@@ -101,4 +168,4 @@ In order to add another storage provider the CS3 storage registry that is runnin
}
```
In the above replace `{storage-users-mount-uuid}` with the mount UUID that was generated for the storage-users service. You can find it in the `config.yaml` generated on by `ocis init`. The last entry `com.owncloud.api.storage-hello` and its `providerid` `"hello-storage-id"` are an example for in additional storage provider, in this case running `hellofs`, an example minimal storage driver.
In the above replace `{storage-users-mount-uuid}` with the mount UUID that was generated for the storage-users service. You can find it in the `config.yaml` generated on by `ocis init`. The last entry `com.owncloud.api.storage-hello` and its `providerid` `"hello-storage-id"` are an example for in additional storage provider, in this case running `hellofs`, an example minimal storage driver.
+1 -1
View File
@@ -81,7 +81,7 @@ func Server(cfg *config.Config) *cli.Command {
cancel()
})
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+15 -15
View File
@@ -31,20 +31,20 @@ type Config struct {
FrontendPublicURL string `yaml:"frontend_public_url" env:"OCIS_URL;GATEWAY_FRONTEND_PUBLIC_URL" desc:"The public facing URL of the oCIS frontend." introductionVersion:"pre5.0"`
UsersEndpoint string `yaml:"-"`
GroupsEndpoint string `yaml:"-"`
PermissionsEndpoint string `yaml:"-"`
SharingEndpoint string `yaml:"-"`
AuthAppEndpoint string `yaml:"-"`
AuthBasicEndpoint string `yaml:"-"`
AuthBearerEndpoint string `yaml:"-"`
AuthMachineEndpoint string `yaml:"-"`
AuthServiceEndpoint string `yaml:"-"`
StoragePublicLinkEndpoint string `yaml:"-"`
StorageUsersEndpoint string `yaml:"-"`
StorageSharesEndpoint string `yaml:"-"`
AppRegistryEndpoint string `yaml:"-"`
OCMEndpoint string `yaml:"-"`
UsersEndpoint string `yaml:"users_endpoint" env:"GATEWAY_USERS_ENDPOINT" desc:"The endpoint of the users service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
GroupsEndpoint string `yaml:"groups_endpoint" env:"GATEWAY_GROUPS_ENDPOINT" desc:"The endpoint of the groups service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
PermissionsEndpoint string `yaml:"permissions_endpoint" env:"GATEWAY_PERMISSIONS_ENDPOINT" desc:"The endpoint of the permissions service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
SharingEndpoint string `yaml:"sharing_endpoint" env:"GATEWAY_SHARING_ENDPOINT" desc:"The endpoint of the shares service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
AuthAppEndpoint string `yaml:"auth_app_endpoint" env:"GATEWAY_AUTH_APP_ENDPOINT" desc:"The endpoint of the auth-app service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
AuthBasicEndpoint string `yaml:"auth_basic_endpoint" env:"GATEWAY_AUTH_BASIC_ENDPOINT" desc:"The endpoint of the auth-basic service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
AuthBearerEndpoint string `yaml:"auth_bearer_endpoint" env:"GATEWAY_AUTH_BEARER_ENDPOINT" desc:"The endpoint of the auth-bearer service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
AuthMachineEndpoint string `yaml:"auth_machine_endpoint" env:"GATEWAY_AUTH_MACHINE_ENDPOINT" desc:"The endpoint of the auth-machine service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
AuthServiceEndpoint string `yaml:"auth_service_endpoint" env:"GATEWAY_AUTH_SERVICE_ENDPOINT" desc:"The endpoint of the auth-service service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
StoragePublicLinkEndpoint string `yaml:"storage_public_link_endpoint" env:"GATEWAY_STORAGE_PUBLIC_LINK_ENDPOINT" desc:"The endpoint of the storage-publiclink service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
StorageUsersEndpoint string `yaml:"storage_users_endpoint" env:"GATEWAY_STORAGE_USERS_ENDPOINT" desc:"The endpoint of the storage-users service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
StorageSharesEndpoint string `yaml:"storage_shares_endpoint" env:"GATEWAY_STORAGE_SHARES_ENDPOINT" desc:"The endpoint of the storag-shares service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
AppRegistryEndpoint string `yaml:"app_registry_endpoint" env:"GATEWAY_APP_REGISTRY_ENDPOINT" desc:"The endpoint of the app-registry service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
OCMEndpoint string `yaml:"ocm_endpoint" env:"GATEWAY_OCM_ENDPOINT" desc:"The endpoint of the ocm service. Can take a service name or a gRPC URI with the dns, kubernetes or unix protocol." introductionVersion:"%%NEXT%%"`
StorageRegistry StorageRegistry `yaml:"storage_registry"` // TODO: should we even support switching this?
@@ -74,7 +74,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"OCIS_GATEWAY_GRPC_ADDR;GATEWAY_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"GATEWAY_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;GATEWAY_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
}
type StorageRegistry struct {
+26
View File
@@ -63,3 +63,29 @@ The client that is used to authenticate with keycloak has to be able to list use
* `view-authorization`
Note that these roles are only available to assign if the client is in the `master` realm.
## Translations
The `graph` service has embedded translations sourced via transifex to provide a basic set of translated languages. These embedded translations are available for all deployment scenarios. In addition, the service supports custom translations, though it is currently not possible to just add custom translations to embedded ones. If custom translations are configured, the embedded ones are not used. To configure custom translations, the `GRAPH_TRANSLATION_PATH` environment variable needs to point to a base folder that will contain the translation files. This path must be available from all instances of the graph service, a shared storage is recommended. Translation files must be of type [.po](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files) or [.mo](https://www.gnu.org/software/gettext/manual/html_node/Binaries.html). For each language, the filename needs to be `graph.po` (or `graph.mo`) and stored in a folder structure defining the language code. In general the path/name pattern for a translation file needs to be:
```text
{GRAPH_TRANSLATION_PATH}/{language-code}/LC_MESSAGES/graph.po
```
The language code pattern is composed of `language[_territory]` where `language` is the base language and `_territory` is optional and defines a country.
For example, for the language `de`, one needs to place the corresponding translation files to `{GRAPH_TRANSLATION_PATH}/de_DE/LC_MESSAGES/graph.po`.
<!-- also see the notifications readme -->
Important: For the time being, the embedded ownCloud Web frontend only supports the main language code but does not handle any territory. When strings are available in the language code `language_territory`, the web frontend does not see it as it only requests `language`. In consequence, any translations made must exist in the requested `language` to avoid a fallback to the default.
### Translation Rules
* If a requested language code is not available, the service tries to fall back to the base language if available. For example, if the requested language-code `de_DE` is not available, the service tries to fall back to translations in the `de` folder.
* If the base language `de` is also not available, the service falls back to the system's default English (`en`),
which is the source of the texts provided by the code.
## Default Language
The default language can be defined via the `OCIS_DEFAULT_LANGUAGE` environment variable. See the `settings` service for a detailed description.
+1
View File
@@ -47,6 +47,7 @@ type Spaces struct {
GroupsCacheTTL int `yaml:"groups_cache_ttl" env:"GRAPH_SPACES_GROUPS_CACHE_TTL" desc:"Max TTL in seconds for the spaces groups cache." introductionVersion:"pre5.0"`
StorageUsersAddress string `yaml:"storage_users_address" env:"GRAPH_SPACES_STORAGE_USERS_ADDRESS" desc:"The address of the storage-users service." introductionVersion:"5.0"`
DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details." introductionVersion:"5.0"`
TranslationPath string `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH;GRAPH_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details." introductionVersion:"%%NEXT%%"`
}
type LDAP struct {
+2 -2
View File
@@ -20,8 +20,8 @@ const (
)
// Translate translates a string based on the locale and default locale
func Translate(content, locale, defaultLocale string) string {
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, "", _localeFS, _localeSubPath)
func Translate(content, locale, defaultLocale, translationPath string) string {
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _localeFS, _localeSubPath)
return t.Translate(content, locale)
}
@@ -73,13 +73,13 @@ const (
)
// NewDriveItemPermissionsService creates a new DriveItemPermissionsService
func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache identity.IdentityCache, c *config.Config) (DriveItemPermissionsService, error) {
func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache identity.IdentityCache, config *config.Config) (DriveItemPermissionsService, error) {
return DriveItemPermissionsService{
BaseGraphService: BaseGraphService{
logger: &log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()},
gatewaySelector: gatewaySelector,
identityCache: identityCache,
config: c,
config: config,
},
}, nil
}
@@ -130,7 +130,7 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto
permission := &libregraph.Permission{}
availableRoles := unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(s.config.UnifiedRoles.AvailableRoles...))
if role := unifiedrole.CS3ResourcePermissionsToRole(availableRoles, cs3ResourcePermissions, condition); role != nil {
if role := unifiedrole.CS3ResourcePermissionsToRole(availableRoles, cs3ResourcePermissions, condition, false); role != nil {
permission.Roles = []string{role.GetId()}
}
@@ -389,6 +389,17 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID
if err != nil {
return collectionOfPermissions, err
}
if s.config.IncludeOCMSharees {
driveItems, err = s.listOCMShares(ctx, []*ocm.ListOCMSharesRequest_Filter{
{
Type: ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID,
Term: &ocm.ListOCMSharesRequest_Filter_ResourceId{ResourceId: itemID},
},
}, driveItems)
if err != nil {
return collectionOfPermissions, err
}
}
}
// finally get public shares, which are possible for spaceroots and "normal" resources
driveItems, err = s.listPublicShares(ctx, []*link.ListPublicSharesRequest_Filter{
+150
View File
@@ -197,6 +197,7 @@ func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, s
availableRoles,
perm,
unifiedrole.UnifiedRoleConditionDrive,
false,
); role != nil {
switch apiVersion {
case APIVersion_1:
@@ -287,6 +288,34 @@ func (g BaseGraphService) listUserShares(ctx context.Context, filters []*collabo
return driveItems, nil
}
func (g BaseGraphService) listOCMShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Error().Err(err).Msg("could not select next gateway client")
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
concreteFilters := []*ocm.ListOCMSharesRequest_Filter{}
concreteFilters = append(concreteFilters, filters...)
lsOCMSharesRequest := ocm.ListOCMSharesRequest{
Filters: concreteFilters,
}
lsOCMSharesResponse, err := gatewayClient.ListOCMShares(ctx, &lsOCMSharesRequest)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
if statusCode := lsOCMSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsOCMSharesResponse.Status.Message)
}
driveItems, err = g.cs3OCMSharesToDriveItems(ctx, lsOCMSharesResponse.Shares, driveItems)
if err != nil {
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
return driveItems, nil
}
func (g BaseGraphService) listPublicShares(ctx context.Context, filters []*link.ListPublicSharesRequest_Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
gatewayClient, err := g.gatewaySelector.Next()
@@ -355,6 +384,42 @@ func (g BaseGraphService) cs3UserSharesToDriveItems(ctx context.Context, shares
}
return driveItems, nil
}
func (g BaseGraphService) cs3OCMSharesToDriveItems(ctx context.Context, shares []*ocm.Share, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
for _, s := range shares {
g.logger.Debug().Interface("CS3 OCMShare", s).Msg("Got Share")
resIDStr := storagespace.FormatResourceID(s.ResourceId)
item, ok := driveItems[resIDStr]
if !ok {
itemptr, err := g.getDriveItem(ctx, &storageprovider.Reference{ResourceId: s.ResourceId})
if err != nil {
g.logger.Debug().Err(err).Interface("Share", s.ResourceId).Msg("could not stat ocm share, skipping")
continue
}
item = *itemptr
}
var condition string
switch {
case item.Folder != nil:
condition = unifiedrole.UnifiedRoleConditionFolderFederatedUser
case item.File != nil:
condition = unifiedrole.UnifiedRoleConditionFileFederatedUser
}
perm, err := g.cs3OCMShareToPermission(ctx, s, condition)
var errcode errorcode.Error
switch {
case errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound:
// The Grantee couldn't be found (user/group does not exist anymore)
continue
case err != nil:
return driveItems, err
}
item.Permissions = append(item.Permissions, *perm)
driveItems[resIDStr] = item
}
return driveItems, nil
}
func (g BaseGraphService) cs3UserShareToPermission(ctx context.Context, share *collaboration.Share, roleCondition string) (*libregraph.Permission, error) {
perm := libregraph.Permission{}
@@ -408,6 +473,7 @@ func (g BaseGraphService) cs3UserShareToPermission(ctx context.Context, share *c
unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(g.config.UnifiedRoles.AvailableRoles...)),
share.GetPermissions().GetPermissions(),
roleCondition,
false,
)
if role != nil {
perm.SetRoles([]string{role.GetId()})
@@ -432,6 +498,90 @@ func (g BaseGraphService) cs3UserShareToPermission(ctx context.Context, share *c
}
return &perm, nil
}
func (g BaseGraphService) cs3OCMShareToPermission(ctx context.Context, share *ocm.Share, roleCondition string) (*libregraph.Permission, error) {
perm := libregraph.Permission{}
perm.SetRoles([]string{})
if roleCondition != unifiedrole.UnifiedRoleConditionDrive {
perm.SetId(share.GetId().GetOpaqueId())
}
grantedTo := libregraph.SharePointIdentitySet{}
// hm or use share.GetShareType() to determine the type of share???
switch share.GetGrantee().GetType() {
case storageprovider.GranteeType_GRANTEE_TYPE_USER:
user, err := cs3UserIdToIdentity(ctx, g.identityCache, share.Grantee.GetUserId())
switch {
case errors.Is(err, identity.ErrNotFound):
g.logger.Warn().Str("userid", share.Grantee.GetUserId().GetOpaqueId()).Msg("User not found by id")
// User does not seem to exist anymore, don't add a permission for this
return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist")
case err != nil:
return nil, errorcode.New(errorcode.GeneralException, err.Error())
default:
grantedTo.SetUser(user)
if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
perm.SetId("u:" + user.GetId())
}
}
case storageprovider.GranteeType_GRANTEE_TYPE_GROUP:
group, err := groupIdToIdentity(ctx, g.identityCache, share.Grantee.GetGroupId().GetOpaqueId())
switch {
case errors.Is(err, identity.ErrNotFound):
g.logger.Warn().Str("groupid", share.Grantee.GetGroupId().GetOpaqueId()).Msg("Group not found by id")
// Group not seem to exist anymore, don't add a permission for this
return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist")
case err != nil:
return nil, errorcode.New(errorcode.GeneralException, err.Error())
default:
grantedTo.SetGroup(group)
if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
perm.SetId("g:" + group.GetId())
}
}
}
// set expiration date
if share.GetExpiration() != nil {
perm.SetExpirationDateTime(cs3TimestampToTime(share.GetExpiration()))
}
// set cTime
if share.GetCtime() != nil {
perm.SetCreatedDateTime(cs3TimestampToTime(share.GetCtime()))
}
var permissions *storageprovider.ResourcePermissions
for _, role := range share.GetAccessMethods() {
if role.GetWebdavOptions().GetPermissions() != nil {
permissions = role.GetWebdavOptions().GetPermissions()
}
}
role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(
permissions,
roleCondition,
true,
)
if role != nil {
perm.SetRoles([]string{role.GetId()})
} else {
actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissions)
perm.SetLibreGraphPermissionsActions(actions)
perm.SetRoles(nil)
}
perm.SetGrantedToV2(grantedTo)
if share.GetCreator() != nil {
identity, err := cs3UserIdToIdentity(ctx, g.identityCache, share.GetCreator())
if err != nil {
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
perm.SetInvitation(
libregraph.SharingInvitation{
InvitedBy: &libregraph.IdentitySet{
User: &identity,
},
},
)
}
return &perm, nil
}
func (g BaseGraphService) cs3PublicSharesToDriveItems(ctx context.Context, shares []*link.PublicShare, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
for _, s := range shares {
@@ -12,7 +12,7 @@ import (
func init() {
r := registry.GetRegistry(registry.Inmemory())
service := registry.BuildGRPCService("com.owncloud.api.gateway", "", "")
service := registry.BuildGRPCService("com.owncloud.api.gateway", "", "", "")
service.Nodes = []*mRegistry.Node{{
Address: "any",
}}
@@ -25,6 +25,14 @@ func (g Graph) GetSharedByMe(w http.ResponseWriter, r *http.Request) {
return
}
if g.config.IncludeOCMSharees {
driveItems, err = g.listOCMShares(ctx, nil, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
}
driveItems, err = g.listPublicShares(ctx, nil, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
@@ -71,7 +71,7 @@ func (g Graph) applyDefaultTemplate(ctx context.Context, gwc gateway.GatewayAPIC
opaque = utils.AppendPlainToOpaque(opaque, SpaceImageSpecialFolderName, iid)
// upload readme.md
rid, err := readmeUpload(ctx, mdc, locale, g.config.Spaces.DefaultLanguage)
rid, err := readmeUpload(ctx, mdc, locale, g.config.Spaces.DefaultLanguage, g.config.Spaces.TranslationPath)
if err != nil {
return err
}
@@ -112,10 +112,10 @@ func imageUpload(ctx context.Context, mdc *metadata.CS3) (string, error) {
return res.FileID, nil
}
func readmeUpload(ctx context.Context, mdc *metadata.CS3, locale string, defaultLocale string) (string, error) {
func readmeUpload(ctx context.Context, mdc *metadata.CS3, locale string, defaultLocale string, translationPath string) (string, error) {
res, err := mdc.Upload(ctx, metadata.UploadRequest{
Path: filepath.Join(_spaceFolderName, _readmeName),
Content: []byte(l10n_pkg.Translate(_readmeText, locale, defaultLocale)),
Content: []byte(l10n_pkg.Translate(_readmeText, locale, defaultLocale, translationPath)),
})
if err != nil {
return "", err
+58 -20
View File
@@ -109,6 +109,22 @@ func userIdToIdentity(ctx context.Context, cache identity.IdentityCache, userID
user, err := cache.GetUser(ctx, userID)
if err == nil {
identity.SetDisplayName(user.GetDisplayName())
identity.SetLibreGraphUserType(user.GetUserType())
}
return identity, err
}
// federatedIdToIdentity looks the user for the supplied id using the cache and returns it
// as a libregraph.Identity
func federatedIdToIdentity(ctx context.Context, cache identity.IdentityCache, userID string) (libregraph.Identity, error) {
identity := libregraph.Identity{
Id: libregraph.PtrString(userID),
LibreGraphUserType: libregraph.PtrString("Federated"),
}
user, err := cache.GetAcceptedUser(ctx, userID)
if err == nil {
identity.SetDisplayName(user.GetDisplayName())
identity.SetLibreGraphUserType(user.GetUserType())
}
return identity, err
}
@@ -116,6 +132,9 @@ func userIdToIdentity(ctx context.Context, cache identity.IdentityCache, userID
// cs3UserIdToIdentity looks up the user for the supplied cs3 userid using the cache and returns it
// as a libregraph.Identity. Skips the user lookup if the id type is USER_TYPE_SPACE_OWNER
func cs3UserIdToIdentity(ctx context.Context, cache identity.IdentityCache, cs3UserID *cs3User.UserId) (libregraph.Identity, error) {
if cs3UserID.GetType() == cs3User.UserType_USER_TYPE_FEDERATED {
return federatedIdToIdentity(ctx, cache, cs3UserID.GetOpaqueId())
}
if cs3UserID.GetType() != cs3User.UserType_USER_TYPE_SPACE_OWNER {
return userIdToIdentity(ctx, cache, cs3UserID.GetOpaqueId())
}
@@ -434,6 +453,7 @@ func cs3ReceivedShareToLibreGraphPermissions(ctx context.Context, logger *log.Lo
availableRoles,
permissionSet,
condition,
false,
)
if role != nil {
permission.SetRoles([]string{role.GetId()})
@@ -479,6 +499,17 @@ func roleConditionForResourceType(ri *storageprovider.ResourceInfo) (string, err
}
}
func federatedRoleConditionForResourceType(ri *storageprovider.ResourceInfo) (string, error) {
switch {
case ri.Type == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER:
return unifiedrole.UnifiedRoleConditionFolderFederatedUser, nil
case ri.Type == storageprovider.ResourceType_RESOURCE_TYPE_FILE:
return unifiedrole.UnifiedRoleConditionFileFederatedUser, nil
default:
return "", errorcode.New(errorcode.InvalidRequest, "unsupported resource type for federated role")
}
}
// ExtractShareIdFromResourceId is a bit of a hack.
// We should not rely on a specific format of the item id.
// But currently there is no other way to get the ShareID.
@@ -752,36 +783,43 @@ func fillDriveItemPropertiesFromReceivedOCMShare(ctx context.Context, logger *lo
func cs3ReceivedOCMShareToLibreGraphPermissions(ctx context.Context, logger *log.Logger,
identityCache identity.IdentityCache, receivedShare *ocm.ReceivedShare,
_ *storageprovider.ResourceInfo) (*libregraph.Permission, error) {
resourceInfo *storageprovider.ResourceInfo) (*libregraph.Permission, error) {
permission := libregraph.NewPermission()
if id := receivedShare.GetId().GetOpaqueId(); id != "" {
permission.SetId(id)
}
if cTime := receivedShare.GetCtime(); cTime != nil {
permission.SetCreatedDateTime(cs3TimestampToTime(cTime))
}
if expiration := receivedShare.GetExpiration(); expiration != nil {
permission.SetExpirationDateTime(cs3TimestampToTime(expiration))
}
/*
if permissionSet := receivedShare.GetShare().GetPermissions().GetPermissions(); permissionSet != nil {
condition, err := roleConditionForResourceType(resourceInfo)
if err != nil {
return nil, err
}
role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(*permissionSet, condition)
if role != nil {
permission.SetRoles([]string{role.GetId()})
}
actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(*permissionSet)
// actions only make sense if no role is set
if role == nil && len(actions) > 0 {
permission.SetLibreGraphPermissionsActions(actions)
}
var permissions *storageprovider.ResourcePermissions
for _, protocol := range receivedShare.GetProtocols() {
if protocol.GetWebdavOptions().GetPermissions() != nil {
permissions = protocol.GetWebdavOptions().GetPermissions().GetPermissions()
}
*/
}
condition, err := federatedRoleConditionForResourceType(resourceInfo)
if err != nil {
return nil, err
}
role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(
permissions,
condition,
true,
)
if role != nil {
permission.SetRoles([]string{role.GetId()})
} else {
actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissions)
permission.SetLibreGraphPermissionsActions(actions)
permission.SetRoles(nil)
}
switch grantee := receivedShare.GetGrantee(); {
case grantee.GetType() == storageprovider.GranteeType_GRANTEE_TYPE_USER:
user, err := cs3UserIdToIdentity(ctx, identityCache, grantee.GetUserId())
+1 -1
View File
@@ -94,7 +94,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -49,7 +49,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"GROUPS_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"GROUPS_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;GROUPS_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
}
type Drivers struct {
+1 -1
View File
@@ -146,7 +146,7 @@
"react-dev-utils": "^12.0.1",
"resolve": "1.22.8",
"resolve-url-loader": "^5.0.0",
"sass-loader": "^10.5.2",
"sass-loader": "^16.0.1",
"source-map-explorer": "^2.5.3",
"typescript": "^4.9.5",
"url-loader": "4.1.1",
+15 -13
View File
@@ -232,8 +232,8 @@ importers:
specifier: ^5.0.0
version: 5.0.0
sass-loader:
specifier: ^10.5.2
version: 10.5.2(webpack@5.93.0)
specifier: ^16.0.1
version: 16.0.1(webpack@5.93.0)
source-map-explorer:
specifier: ^2.5.3
version: 2.5.3
@@ -5219,21 +5219,26 @@ packages:
sanitize.css@13.0.0:
resolution: {integrity: sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==}
sass-loader@10.5.2:
resolution: {integrity: sha512-vMUoSNOUKJILHpcNCCyD23X34gve1TS7Rjd9uXHeKqhvBG39x6XbswFDtpbTElj6XdMFezoWhkh5vtKudf2cgQ==}
engines: {node: '>= 10.13.0'}
sass-loader@16.0.1:
resolution: {integrity: sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==}
engines: {node: '>= 18.12.0'}
peerDependencies:
fibers: '>= 3.1.0'
'@rspack/core': 0.x || 1.x
node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
sass: ^1.3.0
webpack: ^4.36.0 || ^5.0.0
sass-embedded: '*'
webpack: ^5.0.0
peerDependenciesMeta:
fibers:
'@rspack/core':
optional: true
node-sass:
optional: true
sass:
optional: true
sass-embedded:
optional: true
webpack:
optional: true
scheduler@0.20.2:
resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==}
@@ -12031,13 +12036,10 @@ snapshots:
sanitize.css@13.0.0: {}
sass-loader@10.5.2(webpack@5.93.0):
sass-loader@16.0.1(webpack@5.93.0):
dependencies:
klona: 2.0.6
loader-utils: 2.0.4
neo-async: 2.6.2
schema-utils: 3.3.0
semver: 7.6.3
optionalDependencies:
webpack: 5.93.0
scheduler@0.20.2:
+1 -1
View File
@@ -46,7 +46,7 @@ type SMTP struct {
Password string `yaml:"smtp_password" env:"NOTIFICATIONS_SMTP_PASSWORD" desc:"Password for the SMTP host to connect to." introductionVersion:"pre5.0"`
Insecure bool `yaml:"insecure" env:"NOTIFICATIONS_SMTP_INSECURE" desc:"Allow insecure connections to the SMTP server." introductionVersion:"pre5.0"`
Authentication string `yaml:"smtp_authentication" env:"NOTIFICATIONS_SMTP_AUTHENTICATION" desc:"Authentication method for the SMTP communication. Possible values are 'login', 'plain', 'crammd5', 'none' or 'auto'. If set to 'auto' or unset, the authentication method is automatically negotiated with the server." introductionVersion:"pre5.0"`
Encryption string `yaml:"smtp_encryption" env:"NOTIFICATIONS_SMTP_ENCRYPTION" desc:"Encryption method for the SMTP communication. Possible values are 'starttls', 'ssl', 'ssltls', 'tls' and 'none'." introductionVersion:"pre5.0" deprecationVersion:"5.0.0" removalVersion:"7.0.0" deprecationInfo:"The NOTIFICATIONS_SMTP_ENCRYPTION values 'ssl' and 'tls' are deprecated and will be removed in the future." deprecationReplacement:"Use 'starttls' instead of 'tls' and 'ssltls' instead of 'ssl'."`
Encryption string `yaml:"smtp_encryption" env:"NOTIFICATIONS_SMTP_ENCRYPTION" desc:"Encryption method for the SMTP communication. Possible values are 'starttls', 'ssl', 'ssltls', 'tls' and 'none'." introductionVersion:"pre5.0" deprecationVersion:"5.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"The NOTIFICATIONS_SMTP_ENCRYPTION values 'ssl' and 'tls' are deprecated and will be removed in the future." deprecationReplacement:"Use 'starttls' instead of 'tls' and 'ssltls' instead of 'ssl'."`
}
// Events combines the configuration options for the event bus.
@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2024-08-06 04:39+0000\n"
"POT-Creation-Date: 2024-08-27 00:04+0000\n"
"PO-Revision-Date: 2023-04-19 11:11+0000\n"
"Last-Translator: hoshi1800 <hoshi1800@gmail.com>, 2024\n"
"Language-Team: Japanese (Japan) (https://app.transifex.com/owncloud-org/teams/6149/ja_JP/)\n"
@@ -12,7 +12,7 @@ import (
func init() {
r := registry.GetRegistry(registry.Inmemory())
service := registry.BuildGRPCService("com.owncloud.api.gateway", "", "")
service := registry.BuildGRPCService("com.owncloud.api.gateway", "", "", "")
service.Nodes = []*mRegistry.Node{{
Address: "any",
}}
+1 -1
View File
@@ -82,7 +82,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -77,7 +77,7 @@ type GRPCConfig struct {
Addr string `ocisConfig:"addr" env:"OCM_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"5.0"`
Namespace string `ocisConfig:"-" yaml:"-"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Protocol string `yaml:"protocol" env:"OCM_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;OCM_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"5.0"`
}
type ScienceMesh struct {
+8 -18
View File
@@ -9,7 +9,6 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/service/http"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
svc "github.com/owncloud/ocis/v2/services/ocs/pkg/service/v0"
ocisstore "github.com/owncloud/ocis/v2/services/store/pkg/store"
"go-micro.dev/v4"
microstore "go-micro.dev/v4/store"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
@@ -37,23 +36,14 @@ func Server(opts ...Option) (http.Service, error) {
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
}
var signingKeyStore microstore.Store
if options.Config.SigningKeys.Store == "ocisstoreservice" {
signingKeyStore = ocisstore.NewStore(
microstore.Nodes(options.Config.SigningKeys.Nodes...),
microstore.Database("proxy"),
microstore.Table("signing-keys"),
)
} else {
signingKeyStore = store.Create(
store.Store(options.Config.SigningKeys.Store),
store.TTL(options.Config.SigningKeys.TTL),
microstore.Nodes(options.Config.SigningKeys.Nodes...),
microstore.Database("proxy"),
microstore.Table("signing-keys"),
store.Authentication(options.Config.SigningKeys.AuthUsername, options.Config.SigningKeys.AuthPassword),
)
}
signingKeyStore := store.Create(
store.Store(options.Config.SigningKeys.Store),
store.TTL(options.Config.SigningKeys.TTL),
microstore.Nodes(options.Config.SigningKeys.Nodes...),
microstore.Database("proxy"),
microstore.Table("signing-keys"),
store.Authentication(options.Config.SigningKeys.AuthUsername, options.Config.SigningKeys.AuthPassword),
)
handle := svc.NewService(
svc.Logger(options.Logger),
+8 -18
View File
@@ -37,7 +37,6 @@ import (
"github.com/owncloud/ocis/v2/services/proxy/pkg/staticroutes"
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
ocisstore "github.com/owncloud/ocis/v2/services/store/pkg/store"
"github.com/urfave/cli/v2"
"go-micro.dev/v4/selector"
microstore "go-micro.dev/v4/store"
@@ -66,23 +65,14 @@ func Server(cfg *config.Config) *cli.Command {
store.Authentication(cfg.OIDC.UserinfoCache.AuthUsername, cfg.OIDC.UserinfoCache.AuthPassword),
)
var signingKeyStore microstore.Store
if cfg.PreSignedURL.SigningKeys.Store == "ocisstoreservice" {
signingKeyStore = ocisstore.NewStore(
microstore.Nodes(cfg.PreSignedURL.SigningKeys.Nodes...),
microstore.Database("proxy"),
microstore.Table("signing-keys"),
)
} else {
signingKeyStore = store.Create(
store.Store(cfg.PreSignedURL.SigningKeys.Store),
store.TTL(cfg.PreSignedURL.SigningKeys.TTL),
microstore.Nodes(cfg.PreSignedURL.SigningKeys.Nodes...),
microstore.Database("proxy"),
microstore.Table("signing-keys"),
store.Authentication(cfg.PreSignedURL.SigningKeys.AuthUsername, cfg.PreSignedURL.SigningKeys.AuthPassword),
)
}
signingKeyStore := store.Create(
store.Store(cfg.PreSignedURL.SigningKeys.Store),
store.TTL(cfg.PreSignedURL.SigningKeys.TTL),
microstore.Nodes(cfg.PreSignedURL.SigningKeys.Nodes...),
microstore.Database("proxy"),
microstore.Table("signing-keys"),
store.Authentication(cfg.PreSignedURL.SigningKeys.AuthUsername, cfg.PreSignedURL.SigningKeys.AuthPassword),
)
logger := logging.Configure(cfg.Service.Name, cfg.Log)
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name)
@@ -10,7 +10,7 @@ import (
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
"github.com/pkg/errors"
@@ -5,7 +5,7 @@ import (
"net/http/httptest"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
@@ -12,7 +12,7 @@ import (
func init() {
r := registry.GetRegistry(registry.Inmemory())
service := registry.BuildGRPCService("com.owncloud.api.gateway", "", "")
service := registry.BuildGRPCService("com.owncloud.api.gateway", "", "", "")
service.Nodes = []*mRegistry.Node{{
Address: "any",
}}
+1 -1
View File
@@ -98,7 +98,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -56,7 +56,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"SHARING_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"SHARING_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;SHARING_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
}
type UserSharingDrivers struct {
@@ -81,7 +81,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
@@ -48,7 +48,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"STORAGE_PUBLICLINK_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"STORAGE_PUBLICLINK_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;STORAGE_PUBLICLINK_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
}
type StorageProvider struct {
@@ -81,7 +81,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+1 -1
View File
@@ -49,5 +49,5 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"STORAGE_SHARES_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"STORAGE_SHARES_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;STORAGE_SHARES_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"pre5.0"`
}
@@ -81,7 +81,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
+6 -5
View File
@@ -18,10 +18,11 @@ type Config struct {
GRPC GRPCConfig `yaml:"grpc"`
HTTP HTTPConfig `yaml:"http"`
TokenManager *TokenManager `yaml:"token_manager"`
Reva *shared.Reva `yaml:"reva"`
SystemUserID string `yaml:"system_user_id" env:"OCIS_SYSTEM_USER_ID" desc:"ID of the oCIS storage-system system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"pre5.0"`
SystemUserAPIKey string `yaml:"system_user_api_key" env:"OCIS_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"pre5.0"`
TokenManager *TokenManager `yaml:"token_manager"`
Reva *shared.Reva `yaml:"reva"`
SystemUserID string `yaml:"system_user_id" env:"OCIS_SYSTEM_USER_ID" desc:"ID of the oCIS storage-system system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"pre5.0"`
SystemUserAPIKey string `yaml:"system_user_api_key" env:"OCIS_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"pre5.0"`
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"STORAGE_SYSTEM_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"pre5.0"`
@@ -60,7 +61,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"STORAGE_SYSTEM_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"STORAGE_SYSTEM_GRPC_PROTOCOL" desc:"The transport protocol of the GPRC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;STORAGE_SYSTEM_GRPC_PROTOCOL" desc:"The transport protocol of the GPRC service." introductionVersion:"pre5.0"`
}
// HTTPConfig holds HTTPConfig config
+1 -1
View File
@@ -92,7 +92,7 @@ func Server(cfg *config.Config) *cli.Command {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Addr, version.GetString())
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
@@ -345,7 +345,7 @@ func restoreTrashBindItem(cfg *config.Config) *cli.Command {
func listRecycle(ctx context.Context, client gateway.GatewayAPIClient, ref provider.Reference) (*provider.ListRecycleResponse, error) {
_retrievingErrorMsg := "trash-bin items retrieving error"
res, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: &ref, Key: "/"})
res, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: &ref, Key: ""})
if err != nil {
return nil, fmt.Errorf("%s %w", _retrievingErrorMsg, err)
}
+1 -1
View File
@@ -75,7 +75,7 @@ type GRPCConfig struct {
Addr string `yaml:"addr" env:"STORAGE_USERS_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"STORAGE_USERS_GRPC_PROTOCOL" desc:"The transport protocol of the GPRC service." introductionVersion:"pre5.0"`
Protocol string `yaml:"protocol" env:"OCIS_GRPC_PROTOCOL;STORAGE_USERS_GRPC_PROTOCOL" desc:"The transport protocol of the GPRC service." introductionVersion:"pre5.0"`
}
// HTTPConfig is the configuration for the http server
@@ -12,7 +12,7 @@ import (
func init() {
r := registry.GetRegistry(registry.Inmemory())
service := registry.BuildGRPCService("com.owncloud.api.gateway", "", "")
service := registry.BuildGRPCService("com.owncloud.api.gateway", "", "", "")
service.Nodes = []*mRegistry.Node{{
Address: "any",
}}
-45
View File
@@ -1,45 +0,0 @@
SHELL := bash
NAME := store
include ../../.make/recursion.mk
############ tooling ############
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
############ go tooling ############
include ../../.make/go.mk
############ release ############
include ../../.make/release.mk
############ docs generate ############
include ../../.make/docs.mk
.PHONY: docs-generate
docs-generate: config-docs-generate \
grpc-docs-generate
############ generate ############
include ../../.make/generate.mk
.PHONY: ci-go-generate
ci-go-generate: protobuf # CI runs ci-node-generate automatically before this target
.PHONY: ci-node-generate
ci-node-generate:
############ protobuf ############
include ../../.make/protobuf.mk
.PHONY: protobuf
protobuf: buf-generate
############ licenses ############
.PHONY: ci-node-check-licenses
ci-node-check-licenses:
.PHONY: ci-node-save-licenses
ci-node-save-licenses:
-19
View File
@@ -1,19 +0,0 @@
package main
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/owncloud/ocis/v2/services/store/pkg/command"
"github.com/owncloud/ocis/v2/services/store/pkg/config/defaults"
)
func main() {
cfg := defaults.DefaultConfig()
cfg.Context, _ = signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP)
if err := command.Execute(cfg); err != nil {
os.Exit(1)
}
}
@@ -1,19 +0,0 @@
FROM amd64/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS Store" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9460
ENTRYPOINT ["/usr/bin/ocis-store"]
CMD ["server"]
COPY bin/ocis-store /usr/bin/ocis-store
@@ -1,19 +0,0 @@
FROM arm32v6/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS Store" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9460
ENTRYPOINT ["/usr/bin/ocis-store"]
CMD ["server"]
COPY bin/ocis-store /usr/bin/ocis-store
@@ -1,19 +0,0 @@
FROM arm64v8/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS Store" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9460
ENTRYPOINT ["/usr/bin/ocis-store"]
CMD ["server"]
COPY bin/ocis-store /usr/bin/ocis-store
-22
View File
@@ -1,22 +0,0 @@
image: owncloud/ocis-store:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: owncloud/ocis-store:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: owncloud/ocis-store:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
variant: v8
os: linux
- image: owncloud/ocis-store:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
variant: v6
os: linux
-54
View File
@@ -1,54 +0,0 @@
package command
import (
"fmt"
"net/http"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
"github.com/owncloud/ocis/v2/services/store/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/store/pkg/logging"
"github.com/urfave/cli/v2"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "health",
Usage: "check health status",
Category: "info",
Before: func(c *cli.Context) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
logger.Fatal().
Err(err).
Msg("Failed to request health check")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Fatal().
Int("code", resp.StatusCode).
Msg("Health seems to be in bad state")
}
logger.Debug().
Int("code", resp.StatusCode).
Msg("Health got a good state")
return nil
},
}
}
-34
View File
@@ -1,34 +0,0 @@
package command
import (
"os"
"github.com/owncloud/ocis/v2/ocis-pkg/clihelper"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
"github.com/urfave/cli/v2"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) cli.Commands {
return []*cli.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the ocis-store command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "store",
Usage: "Service to store values for ocis services",
Commands: GetCommands(cfg),
})
return app.RunContext(cfg.Context, os.Args)
}
-94
View File
@@ -1,94 +0,0 @@
package command
import (
"context"
"fmt"
"github.com/oklog/run"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
"github.com/owncloud/ocis/v2/services/store/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/store/pkg/logging"
"github.com/owncloud/ocis/v2/services/store/pkg/metrics"
"github.com/owncloud/ocis/v2/services/store/pkg/server/debug"
"github.com/owncloud/ocis/v2/services/store/pkg/server/grpc"
"github.com/urfave/cli/v2"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
Category: "server",
Before: func(c *cli.Context) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name)
if err != nil {
return err
}
cfg.GrpcClient, err = ogrpc.NewClient(
append(ogrpc.GetClientOptions(cfg.GRPCClientTLS), ogrpc.WithTraceProvider(traceProvider))...,
)
if err != nil {
return err
}
var (
gr = run.Group{}
ctx, cancel = context.WithCancel(c.Context)
metrics = metrics.New()
)
defer cancel()
metrics.BuildInfo.WithLabelValues(version.GetString()).Set(1)
{
server := grpc.Server(
grpc.Logger(logger),
grpc.Context(ctx),
grpc.Config(cfg),
grpc.Metrics(metrics),
grpc.TraceProvider(traceProvider),
)
gr.Add(server.Run, func(err error) {
logger.Error().
Err(err).
Str("server", "grpc").
Msg("Shutting down server")
cancel()
})
}
{
server, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Error().Err(err).Str("server", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(server.ListenAndServe, func(_ error) {
_ = server.Shutdown(ctx)
cancel()
})
}
return gr.Run()
},
}
}
-50
View File
@@ -1,50 +0,0 @@
package command
import (
"fmt"
"os"
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
"github.com/urfave/cli/v2"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "version",
Usage: "print the version of this binary and the running service instances",
Category: "info",
Action: func(c *cli.Context) error {
fmt.Println("Version: " + version.GetString())
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.GRPC.Namespace + "." + cfg.Service.Name)
if err != nil {
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
return err
}
if len(services) == 0 {
fmt.Println("No running " + cfg.Service.Name + " service found.")
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
-28
View File
@@ -1,28 +0,0 @@
package config
import (
"context"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"go-micro.dev/v4/client"
)
// Config combines all available configuration parts.
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
Tracing *Tracing `yaml:"tracing"`
Log *Log `yaml:"log"`
Debug Debug `yaml:"debug"`
GRPC GRPCConfig `yaml:"grpc"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
GrpcClient client.Client `yaml:"-"`
Datapath string `yaml:"data_path" env:"STORE_DATA_PATH" desc:"The directory where the filesystem storage will store ocis settings. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/store." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Context context.Context `yaml:"-"`
}
-9
View File
@@ -1,9 +0,0 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"STORE_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Token string `yaml:"token" env:"STORE_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Pprof bool `yaml:"pprof" env:"STORE_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Zpages bool `yaml:"zpages" env:"STORE_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
}
@@ -1,75 +0,0 @@
package defaults
import (
"path"
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
"github.com/owncloud/ocis/v2/ocis-pkg/structs"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
)
// FullDefaultConfig returns a fully initialized default configuration
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
// DefaultConfig returns a basic default configuration
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9464",
Token: "",
Pprof: false,
Zpages: false,
},
GRPC: config.GRPCConfig{
Addr: "127.0.0.1:9460",
Namespace: "com.owncloud.api",
},
Service: config.Service{
Name: "store",
},
Datapath: path.Join(defaults.BaseDataPath(), "store"),
}
}
// EnsureDefaults adds default values to the configuration if they are not set yet
func EnsureDefaults(cfg *config.Config) {
// provide with defaults for shared logging, since we need a valid destination address for "envdecode".
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
cfg.Log = &config.Log{
Level: cfg.Commons.Log.Level,
Pretty: cfg.Commons.Log.Pretty,
Color: cfg.Commons.Log.Color,
File: cfg.Commons.Log.File,
}
} else if cfg.Log == nil {
cfg.Log = &config.Log{}
}
// provide with defaults for shared tracing, since we need a valid destination address for "envdecode".
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
cfg.Tracing = &config.Tracing{
Enabled: cfg.Commons.Tracing.Enabled,
Type: cfg.Commons.Tracing.Type,
Endpoint: cfg.Commons.Tracing.Endpoint,
Collector: cfg.Commons.Tracing.Collector,
}
} else if cfg.Tracing == nil {
cfg.Tracing = &config.Tracing{}
}
if cfg.GRPCClientTLS == nil && cfg.Commons != nil {
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
}
if cfg.GRPC.TLS == nil && cfg.Commons != nil {
cfg.GRPC.TLS = structs.CopyOrZeroValue(cfg.Commons.GRPCServiceTLS)
}
}
// Sanitize sanitized the configuration
func Sanitize(cfg *config.Config) {
// nothing to sanitize here atm
}
-10
View File
@@ -1,10 +0,0 @@
package config
import "github.com/owncloud/ocis/v2/ocis-pkg/shared"
// GRPCConfig defines the available grpc configuration.
type GRPCConfig struct {
Addr string `yaml:"addr" env:"STORE_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Namespace string `yaml:"-"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
}
-9
View File
@@ -1,9 +0,0 @@
package config
// Log defines the available log configuration.
type Log struct {
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;STORE_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;STORE_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;STORE_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
File string `mapstructure:"file" env:"OCIS_LOG_FILE;STORE_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
}
-38
View File
@@ -1,38 +0,0 @@
package parser
import (
"errors"
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
"github.com/owncloud/ocis/v2/services/store/pkg/config/defaults"
"github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode"
)
// ParseConfig loads configuration from known paths.
func ParseConfig(cfg *config.Config) error {
err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
// sanitize config
defaults.Sanitize(cfg)
return Validate(cfg)
}
func Validate(cfg *config.Config) error {
return nil
}
-6
View File
@@ -1,6 +0,0 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}
-21
View File
@@ -1,21 +0,0 @@
package config
import "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
// Tracing defines the available tracing configuration.
type Tracing struct {
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;STORE_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;STORE_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;STORE_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;STORE_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset." introductionVersion:"pre5.0" deprecationVersion:"5.0" removalVersion:"7.0.0" deprecationInfo:"The store service is optional and will be removed."`
}
// Convert Tracing to the tracing package's Config struct.
func (t Tracing) Convert() tracing.Config {
return tracing.Config{
Enabled: t.Enabled,
Type: t.Type,
Endpoint: t.Endpoint,
Collector: t.Collector,
}
}
-17
View File
@@ -1,17 +0,0 @@
package logging
import (
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
)
// Configure initializes a service-specific logger instance.
func Configure(name string, cfg *config.Log) log.Logger {
return log.NewLogger(
log.Name(name),
log.Level(cfg.Level),
log.Pretty(cfg.Pretty),
log.Color(cfg.Color),
log.File(cfg.File),
)
}
-45
View File
@@ -1,45 +0,0 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "ocis"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "store"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
// Counter *prometheus.CounterVec
BuildInfo *prometheus.GaugeVec
}
// New initializes the available metrics.
func New() *Metrics {
m := &Metrics{
// Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
// Namespace: Namespace,
// Subsystem: Subsystem,
// Name: "greet_total",
// Help: "How many greeting requests processed",
// }, []string{}),
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "build_info",
Help: "Build Information",
}, []string{"version"}),
}
// prometheus.Register(
// m.Counter,
// )
_ = prometheus.Register(
m.BuildInfo,
)
return m
}
-50
View File
@@ -1,50 +0,0 @@
package debug
import (
"context"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
-59
View File
@@ -1,59 +0,0 @@
package debug
import (
"io"
"net/http"
"github.com/owncloud/ocis/v2/ocis-pkg/service/debug"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.GetString()),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
debug.Health(health(options.Config)),
debug.Ready(ready(options.Config)),
), nil
}
// health implements the health check.
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does, we want to know.
if err != nil {
panic(err)
}
}
}
// ready implements the ready check.
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does, we want to know.
if err != nil {
panic(err)
}
}
}
-80
View File
@@ -1,80 +0,0 @@
package grpc
import (
"context"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
"github.com/owncloud/ocis/v2/services/store/pkg/metrics"
"go.opentelemetry.io/otel/trace"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Name string
Logger log.Logger
Context context.Context
Config *config.Config
Metrics *metrics.Metrics
TraceProvider trace.TracerProvider
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Name provides a name for the service.
func Name(val string) Option {
return func(o *Options) {
o.Name = val
}
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Metrics provides a function to set the metrics option.
func Metrics(val *metrics.Metrics) Option {
return func(o *Options) {
o.Metrics = val
}
}
// TraceProvider provides a function to configure the trace provider
func TraceProvider(traceProvider trace.TracerProvider) Option {
return func(o *Options) {
if traceProvider != nil {
o.TraceProvider = traceProvider
} else {
o.TraceProvider = trace.NewNoopTracerProvider()
}
}
}
-46
View File
@@ -1,46 +0,0 @@
package grpc
import (
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
svc "github.com/owncloud/ocis/v2/services/store/pkg/service/v0"
)
// Server initializes a new go-micro service ready to run
func Server(opts ...Option) grpc.Service {
options := newOptions(opts...)
service, err := grpc.NewServiceWithClient(
options.Config.GrpcClient,
grpc.TLSEnabled(options.Config.GRPC.TLS.Enabled),
grpc.TLSCert(
options.Config.GRPC.TLS.Cert,
options.Config.GRPC.TLS.Key,
),
grpc.Namespace(options.Config.GRPC.Namespace),
grpc.Name(options.Config.Service.Name),
grpc.Version(version.GetString()),
grpc.Context(options.Context),
grpc.Address(options.Config.GRPC.Addr),
grpc.Logger(options.Logger),
grpc.TraceProvider(options.TraceProvider),
)
if err != nil {
options.Logger.Fatal().Err(err).Msg("Error creating store service")
return grpc.Service{}
}
hdlr, err := svc.New(
svc.Logger(options.Logger),
svc.Config(options.Config),
)
if err != nil {
options.Logger.Fatal().Err(err).Msg("could not initialize service handler")
}
if err = storesvc.RegisterStoreHandler(service.Server(), hdlr); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register service handler")
}
return service
}
-63
View File
@@ -1,63 +0,0 @@
package service
import (
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
Database, Table string
Nodes []string
}
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Database configures the database option.
func Database(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Table configures the Table option.
func Table(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Nodes configures the Nodes option.
func Nodes(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Config configures the Config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
-332
View File
@@ -1,332 +0,0 @@
package service
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
storemsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/store/v0"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
"github.com/owncloud/ocis/v2/services/store/pkg/config"
merrors "go-micro.dev/v4/errors"
"google.golang.org/protobuf/encoding/protojson"
)
// BleveDocument wraps the generated Record.Metadata and adds a property that is used to distinguish documents in the index.
type BleveDocument struct {
Metadata map[string]*storemsg.Field `json:"metadata"`
Database string `json:"database"`
Table string `json:"table"`
}
// New returns a new instance of Service
func New(opts ...Option) (s *Service, err error) {
options := newOptions(opts...)
logger := options.Logger
cfg := options.Config
recordsDir := filepath.Join(cfg.Datapath, "databases")
{
var fi os.FileInfo
if fi, err = os.Stat(recordsDir); err != nil {
if os.IsNotExist(err) {
// create store directory
if err = os.MkdirAll(recordsDir, 0700); err != nil {
return nil, err
}
}
} else if !fi.IsDir() {
return nil, fmt.Errorf("%s is not a directory", recordsDir)
}
}
indexMapping := bleve.NewIndexMapping()
// keep all symbols in terms to allow exact matching, eg. emails
indexMapping.DefaultAnalyzer = keyword.Name
s = &Service{
id: cfg.GRPC.Namespace + "." + cfg.Service.Name,
log: logger,
Config: cfg,
}
indexDir := filepath.Join(cfg.Datapath, "index.bleve")
// for now recreate index on every start
if err = os.RemoveAll(indexDir); err != nil {
return nil, err
}
if s.index, err = bleve.New(indexDir, indexMapping); err != nil {
return
}
if err = s.indexRecords(recordsDir); err != nil {
return nil, err
}
return
}
// Service implements the AccountsServiceHandler interface
type Service struct {
id string
log log.Logger
Config *config.Config
index bleve.Index
}
// Read implements the StoreHandler interface.
func (s *Service) Read(c context.Context, rreq *storesvc.ReadRequest, rres *storesvc.ReadResponse) error {
if len(rreq.Key) != 0 {
id := getID(rreq.Options.Database, rreq.Options.Table, rreq.Key)
file := filepath.Join(s.Config.Datapath, "databases", id)
var data []byte
rec := &storemsg.Record{}
data, err := os.ReadFile(file)
if err != nil {
return merrors.NotFound(s.id, "could not read record")
}
if err = protojson.Unmarshal(data, rec); err != nil {
return merrors.InternalServerError(s.id, "could not unmarshal record")
}
rres.Records = append(rres.Records, rec)
return nil
}
s.log.Info().Interface("request", rreq).Msg("read request")
if rreq.Options.Where != nil {
// build bleve query
// execute search
// fetch the actual record if there's a hit
dtq := bleve.NewTermQuery(rreq.Options.Database)
ttq := bleve.NewTermQuery(rreq.Options.Table)
dtq.SetField("database")
ttq.SetField("table")
query := bleve.NewConjunctionQuery(dtq, ttq)
for k, v := range rreq.Options.Where {
ntq := bleve.NewTermQuery(v.Value)
ntq.SetField("metadata." + k + ".value")
query.AddQuery(ntq)
}
searchRequest := bleve.NewSearchRequest(query)
var searchResult *bleve.SearchResult
searchResult, err := s.index.Search(searchRequest)
if err != nil {
s.log.Error().Err(err).Msg("could not execute bleve search")
return merrors.InternalServerError(s.id, "could not execute bleve search: %v", err.Error())
}
for _, hit := range searchResult.Hits {
rec := &storemsg.Record{}
dest := filepath.Join(s.Config.Datapath, "databases", hit.ID)
var data []byte
data, err := os.ReadFile(dest)
s.log.Info().Str("path", dest).Interface("hit", hit).Msgf("hit info")
if err != nil {
s.log.Info().Str("path", dest).Interface("hit", hit).Msgf("file not found")
return merrors.NotFound(s.id, "could not read record")
}
if err = protojson.Unmarshal(data, rec); err != nil {
return merrors.InternalServerError(s.id, "could not unmarshal record")
}
rres.Records = append(rres.Records, rec)
}
return nil
}
return merrors.InternalServerError(s.id, "neither id nor metadata present")
}
// Write implements the StoreHandler interface.
func (s *Service) Write(c context.Context, wreq *storesvc.WriteRequest, wres *storesvc.WriteResponse) error {
id := getID(wreq.Options.Database, wreq.Options.Table, wreq.Record.Key)
file := filepath.Join(s.Config.Datapath, "databases", id)
var bytes []byte
bytes, err := protojson.Marshal(wreq.Record)
if err != nil {
return merrors.InternalServerError(s.id, "could not marshal record")
}
err = os.MkdirAll(filepath.Dir(file), 0700)
if err != nil {
return err
}
err = os.WriteFile(file, bytes, 0600)
if err != nil {
return merrors.InternalServerError(s.id, "could not write record")
}
doc := BleveDocument{
Metadata: wreq.Record.Metadata,
Database: wreq.Options.Database,
Table: wreq.Options.Table,
}
if err := s.index.Index(id, doc); err != nil {
s.log.Error().Err(err).Interface("document", doc).Msg("could not index record metadata")
return err
}
return nil
}
// Delete implements the StoreHandler interface.
func (s *Service) Delete(c context.Context, dreq *storesvc.DeleteRequest, dres *storesvc.DeleteResponse) error {
id := getID(dreq.Options.Database, dreq.Options.Table, dreq.Key)
file := filepath.Join(s.Config.Datapath, "databases", id)
if err := os.Remove(file); err != nil {
if os.IsNotExist(err) {
return merrors.NotFound(s.id, "could not find record")
}
return merrors.InternalServerError(s.id, "could not delete record")
}
if err := s.index.Delete(id); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not remove record from index")
return merrors.InternalServerError(s.id, "could not remove record from index")
}
return nil
}
// List implements the StoreHandler interface.
func (s *Service) List(context.Context, *storesvc.ListRequest, storesvc.Store_ListStream) error {
return nil
}
// Databases implements the StoreHandler interface.
func (s *Service) Databases(c context.Context, dbreq *storesvc.DatabasesRequest, dbres *storesvc.DatabasesResponse) error {
file := filepath.Join(s.Config.Datapath, "databases")
f, err := os.Open(file)
if err != nil {
return merrors.InternalServerError(s.id, "could not open database directory")
}
defer f.Close()
dnames, err := f.Readdirnames(0)
if err != nil {
return merrors.InternalServerError(s.id, "could not read database directory")
}
dbres.Databases = dnames
return nil
}
// Tables implements the StoreHandler interface.
func (s *Service) Tables(ctx context.Context, in *storesvc.TablesRequest, out *storesvc.TablesResponse) error {
file := filepath.Join(s.Config.Datapath, "databases", in.Database)
f, err := os.Open(file)
if err != nil {
return merrors.InternalServerError(s.id, "could not open tables directory")
}
defer f.Close()
tnames, err := f.Readdirnames(0)
if err != nil {
return merrors.InternalServerError(s.id, "could not read tables directory")
}
out.Tables = tnames
return nil
}
// TODO sanitize key. As it may contain invalid characters, such as slashes.
// file: /tmp/ocis-store/databases/{database}/{table}/{record.key}.
func getID(database string, table string, key string) string {
// TODO sanitize input.
return filepath.Join(database, table, key)
}
func (s Service) indexRecords(recordsDir string) (err error) {
// TODO use filepath.Walk to clean up code
rh, err := os.Open(recordsDir)
if err != nil {
return merrors.InternalServerError(s.id, "could not open database directory")
}
defer rh.Close()
dbs, err := rh.Readdirnames(0)
if err != nil {
return merrors.InternalServerError(s.id, "could not read databases directory")
}
for i := range dbs {
tp := filepath.Join(s.Config.Datapath, "databases", dbs[i])
th, err := os.Open(tp)
if err != nil {
s.log.Error().Err(err).Str("database", dbs[i]).Msg("could not open database directory")
continue
}
defer th.Close()
tables, err := th.Readdirnames(0)
if err != nil {
s.log.Error().Err(err).Str("database", dbs[i]).Msg("could not read database directory")
continue
}
for j := range tables {
tp := filepath.Join(s.Config.Datapath, "databases", dbs[i], tables[j])
kh, err := os.Open(tp)
if err != nil {
s.log.Error().Err(err).Str("database", dbs[i]).Str("table", tables[i]).Msg("could not open table directory")
continue
}
defer kh.Close()
keys, err := kh.Readdirnames(0)
if err != nil {
s.log.Error().Err(err).Str("database", dbs[i]).Str("table", tables[i]).Msg("could not read table directory")
continue
}
for k := range keys {
id := getID(dbs[i], tables[j], keys[k])
kp := filepath.Join(s.Config.Datapath, "databases", id)
// read record
var data []byte
rec := &storemsg.Record{}
data, err = os.ReadFile(kp)
if err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not read record")
continue
}
if err = protojson.Unmarshal(data, rec); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not unmarshal record")
continue
}
// index record
doc := BleveDocument{
Metadata: rec.Metadata,
Database: dbs[i],
Table: tables[j],
}
if err := s.index.Index(id, doc); err != nil {
s.log.Error().Err(err).Interface("document", doc).Str("id", id).Msg("could not index record metadata")
continue
}
s.log.Debug().Str("id", id).Msg("indexed record")
}
}
}
return
}
-20
View File
@@ -1,20 +0,0 @@
package store
import (
"context"
"go-micro.dev/v4/client"
"go-micro.dev/v4/store"
)
type grpcClientContextKey struct{}
// WithGRPCClient sets the grpc client
func WithGRPCClient(c client.Client) store.Option {
return func(o *store.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, grpcClientContextKey{}, c)
}
}
-189
View File
@@ -1,189 +0,0 @@
package store
import (
"context"
"errors"
"net/http"
"time"
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
storemsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/store/v0"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
"go-micro.dev/v4/client"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/logger"
"go-micro.dev/v4/store"
"go-micro.dev/v4/util/cmd"
)
// DefaultDatabase is the namespace that the store
// will use if no namespace is provided.
var (
DefaultDatabase = "proxy"
DefaultTable = "signing-keys"
)
type oss struct {
ctx context.Context
options store.Options
svc storesvc.StoreService
}
func init() {
cmd.DefaultStores["ocisstoreservice"] = NewStore
}
// NewStore returns a micro store.Store wrapper to access the micro store service.
// It only implements the minimal Read and Write options that are used by the proxy and ocs services
// Deprecated: use a different micro.Store implementation like nats-js-ks
func NewStore(opts ...store.Option) store.Store {
options := store.Options{
Context: context.Background(),
Database: DefaultDatabase,
Table: DefaultTable,
Logger: logger.DefaultLogger,
Nodes: []string{"com.owncloud.api.store"},
}
for _, o := range opts {
o(&options)
}
c, ok := options.Context.Value(grpcClientContextKey{}).(client.Client)
if !ok {
var err error
c, err = grpc.NewClient()
if err != nil {
options.Logger.Fields(map[string]interface{}{"err": err}).Log(logger.FatalLevel, "ocisstoreservice could not create new grpc client")
}
}
svc := storesvc.NewStoreService(options.Nodes[0], c)
s := &oss{
ctx: context.Background(),
options: options,
svc: svc,
}
return s
}
// Init initializes the store by configuring a storeservice and initializing
// a grpc client if it has not been passed as a context option.
func (s *oss) Init(opts ...store.Option) error {
for _, o := range opts {
o(&s.options)
}
return s.configure()
}
func (s *oss) configure() error {
c, ok := s.options.Context.Value(grpcClientContextKey{}).(client.Client)
if !ok {
var err error
c, err = grpc.NewClient()
if err != nil {
logger.Fatal("ocisstoreservice could not create new grpc client:", err)
}
}
if len(s.options.Nodes) < 1 {
return errors.New("no node configured")
}
s.svc = storesvc.NewStoreService(s.options.Nodes[0], c)
return nil
}
// Options allows you to view the current options.
func (s *oss) Options() store.Options {
return s.options
}
// Read takes a single key name and optional ReadOptions. It returns matching []*Record or an error.
// Only database and table options are used.
func (s *oss) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
options := store.ReadOptions{
Database: s.options.Database,
Table: s.options.Table,
}
for _, o := range opts {
o(&options)
}
res, err := s.svc.Read(context.Background(), &storesvc.ReadRequest{
Options: &storemsg.ReadOptions{
Database: options.Database,
Table: options.Table,
// Other options ignored
},
Key: key,
})
if err != nil {
e := merrors.Parse(err.Error())
if e.Code == http.StatusNotFound {
return nil, store.ErrNotFound
}
return nil, err
}
records := make([]*store.Record, 0, len(res.Records))
for _, record := range res.Records {
r := &store.Record{
Key: record.Key,
Value: record.Value,
Metadata: map[string]interface{}{},
Expiry: time.Duration(record.Expiry),
}
for k, v := range record.Metadata {
r.Metadata[k] = v.Value // we only support string
}
records = append(records, r)
}
return records, nil
}
// Write() writes a record to the store, and returns an error if the record was not written.
func (s *oss) Write(r *store.Record, opts ...store.WriteOption) error {
options := store.WriteOptions{
Database: s.options.Database,
Table: s.options.Table,
}
for _, o := range opts {
o(&options)
}
_, err := s.svc.Write(context.Background(), &storesvc.WriteRequest{
Options: &storemsg.WriteOptions{
Database: options.Database,
Table: options.Table,
},
Record: &storemsg.Record{
Key: r.Key,
Value: r.Value,
// No expiry supported
},
})
return err
}
// Delete is not implemented for the ocis service
func (s *oss) Delete(key string, opts ...store.DeleteOption) error {
return errors.ErrUnsupported
}
// List is not implemented for the ocis service
func (s *oss) List(opts ...store.ListOption) ([]string, error) {
return nil, errors.ErrUnsupported
}
// Close does nothing
func (s *oss) Close() error {
return nil
}
// String returns the name of the implementation.
func (s *oss) String() string {
return "ocisstoreservice"
}

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