Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package app
import (
"context"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
// Registry is the interface that application registries implement
// for discovering application providers
type Registry interface {
FindProviders(ctx context.Context, mimeType string) ([]*registry.ProviderInfo, error)
ListProviders(ctx context.Context) ([]*registry.ProviderInfo, error)
ListSupportedMimeTypes(ctx context.Context) ([]*registry.MimeTypeInfo, error)
AddProvider(ctx context.Context, p *registry.ProviderInfo) error
GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registry.ProviderInfo, error)
SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registry.ProviderInfo) error
}
// Provider is the interface that application providers implement
// for interacting with external apps that serve the requested resource.
type Provider interface {
GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error)
GetAppProviderInfo(ctx context.Context) (*registry.ProviderInfo, error)
}
@@ -0,0 +1,76 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package demo
import (
"context"
"fmt"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/provider/registry"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
)
func init() {
registry.Register("demo", New)
}
type demoProvider struct {
iframeUIProvider string
}
func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error) {
url := fmt.Sprintf("<iframe src=%s/open/%s?view-mode=%s&access-token=%s />", p.iframeUIProvider, storagespace.FormatResourceID(resource.Id), viewMode.String(), token)
return &appprovider.OpenInAppURL{
AppUrl: url,
Method: "GET",
}, nil
}
func (p *demoProvider) GetAppProviderInfo(ctx context.Context) (*appregistry.ProviderInfo, error) {
return &appregistry.ProviderInfo{
Name: "demo-app",
}, nil
}
type config struct {
IFrameUIProvider string `mapstructure:"iframe_ui_provider"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
// New returns an implementation to of the app.Provider interface that
// connects to an application in the backend.
func New(m map[string]interface{}) (app.Provider, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
return &demoProvider{iframeUIProvider: c.IFrameUIProvider}, nil
}
@@ -0,0 +1,26 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core application providers.
_ "github.com/opencloud-eu/reva/v2/pkg/app/provider/demo"
_ "github.com/opencloud-eu/reva/v2/pkg/app/provider/wopi"
// Add your own here
)
@@ -0,0 +1,34 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import "github.com/opencloud-eu/reva/v2/pkg/app"
// NewFunc is the function that app provider implementations
// should register to at init time.
type NewFunc func(map[string]interface{}) (app.Provider, error)
// NewFuncs is a map containing all the registered app providers.
var NewFuncs = map[string]NewFunc{}
// Register registers a new app provider new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
@@ -0,0 +1,492 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package wopi
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/beevik/etree"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/provider/registry"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/mime"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/templates"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/pkg/errors"
)
func init() {
registry.Register("wopi", New)
}
type config struct {
IOPSecret string `mapstructure:"iop_secret" docs:";The IOP secret used to connect to the wopiserver."`
WopiURL string `mapstructure:"wopi_url" docs:";The wopiserver's URL."`
WopiFolderURLBaseURL string `mapstructure:"wopi_folder_url_base_url" docs:";The base URL to generate links to navigate back to the containing folder."`
WopiFolderURLPathTemplate string `mapstructure:"wopi_folder_url_path_template" docs:";The template to generate the folderurl path segments."`
AppName string `mapstructure:"app_name" docs:";The App user-friendly name."`
AppIconURI string `mapstructure:"app_icon_uri" docs:";A URI to a static asset which represents the app icon."`
AppURL string `mapstructure:"app_url" docs:";The App URL."`
AppIntURL string `mapstructure:"app_int_url" docs:";The internal app URL in case of dockerized deployments. Defaults to AppURL"`
AppAPIKey string `mapstructure:"app_api_key" docs:";The API key used by the app, if applicable."`
JWTSecret string `mapstructure:"jwt_secret" docs:";The JWT secret to be used to retrieve the token TTL."`
AppDesktopOnly bool `mapstructure:"app_desktop_only" docs:"false;Specifies if the app can be opened only on desktop."`
InsecureConnections bool `mapstructure:"insecure_connections"`
AppDisableChat bool `mapstructure:"app_disable_chat"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
type wopiProvider struct {
conf *config
wopiClient *http.Client
appURLs map[string]map[string]string // map[viewMode]map[extension]appURL
}
// New returns an implementation of the app.Provider interface that
// connects to an application in the backend.
func New(m map[string]interface{}) (app.Provider, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
if c.AppIntURL == "" {
c.AppIntURL = c.AppURL
}
if c.IOPSecret == "" {
c.IOPSecret = os.Getenv("REVA_APPPROVIDER_IOPSECRET")
}
c.JWTSecret = sharedconf.GetJWTSecret(c.JWTSecret)
appURLs, err := getAppURLs(c)
if err != nil {
return nil, err
}
wopiClient := rhttp.GetHTTPClient(
rhttp.Timeout(time.Duration(5*int64(time.Second))),
rhttp.Insecure(c.InsecureConnections),
)
wopiClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
return &wopiProvider{
conf: c,
wopiClient: wopiClient,
appURLs: appURLs,
}, nil
}
func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error) {
log := appctx.GetLogger(ctx)
ext := path.Ext(resource.Path)
wopiurl, err := url.Parse(p.conf.WopiURL)
if err != nil {
return nil, err
}
wopiurl.Path = path.Join(wopiurl.Path, "/wopi/iop/openinapp")
httpReq, err := rhttp.NewRequest(ctx, "GET", wopiurl.String(), nil)
if err != nil {
return nil, err
}
q := httpReq.URL.Query()
q.Add("endpoint", storagespace.FormatStorageID(resource.GetId().GetStorageId(), resource.GetId().GetSpaceId()))
q.Add("fileid", resource.GetId().OpaqueId)
q.Add("viewmode", viewMode.String())
folderURLPath := templates.WithResourceInfo(resource, p.conf.WopiFolderURLPathTemplate)
folderURLBaseURL, err := url.Parse(p.conf.WopiFolderURLBaseURL)
if err != nil {
return nil, err
}
if folderURLPath != "" {
folderURLBaseURL.Path = path.Join(folderURLBaseURL.Path, folderURLPath)
q.Add("folderurl", folderURLBaseURL.String())
}
u, ok := ctxpkg.ContextGetUser(ctx)
if ok { // else defaults to "Guest xyz"
if u.Id.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT || u.Id.Type == userpb.UserType_USER_TYPE_FEDERATED {
q.Add("userid", resource.Owner.OpaqueId+"@"+resource.Owner.Idp)
} else {
q.Add("userid", u.Id.OpaqueId+"@"+u.Id.Idp)
}
var isPublicShare bool
if u.Opaque != nil {
if _, ok := u.Opaque.Map["public-share-role"]; ok {
isPublicShare = true
}
}
if !isPublicShare {
q.Add("username", u.DisplayName)
}
}
q.Add("appname", p.conf.AppName)
var viewAppURL string
if viewAppURLs, ok := p.appURLs["view"]; ok {
if viewAppURL, ok = viewAppURLs[ext]; ok {
q.Add("appviewurl", viewAppURL)
}
}
access := "edit"
if resource.GetSize() == 0 {
if _, ok := p.appURLs["editnew"]; ok {
access = "editnew"
}
}
if editAppURLs, ok := p.appURLs[access]; ok {
if editAppURL, ok := editAppURLs[ext]; ok {
q.Add("appurl", editAppURL)
}
}
if q.Get("appurl") == "" {
// assuming that a 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
q.Add("appurl", viewAppURL)
}
if q.Get("appurl") == "" && q.Get("appviewurl") == "" {
return nil, errors.New("wopi: neither edit nor view app url found")
}
if p.conf.AppIntURL != "" {
q.Add("appinturl", p.conf.AppIntURL)
}
httpReq.URL.RawQuery = q.Encode()
if p.conf.AppAPIKey != "" {
httpReq.Header.Set("ApiKey", p.conf.AppAPIKey)
}
httpReq.Header.Set("Authorization", "Bearer "+p.conf.IOPSecret)
httpReq.Header.Set("TokenHeader", token)
// Call the WOPI server and parse the response (body will always contain a payload)
openRes, err := p.wopiClient.Do(httpReq)
if err != nil {
return nil, errors.Wrap(err, "wopi: error performing open request to WOPI server")
}
defer openRes.Body.Close()
body, err := io.ReadAll(openRes.Body)
if err != nil {
return nil, err
}
if openRes.StatusCode != http.StatusOK {
// WOPI returned failure: body contains a user-friendly error message (yet perform a sanity check)
sbody := ""
if body != nil {
sbody = string(body)
}
log.Warn().Msg(fmt.Sprintf("wopi: WOPI server returned HTTP %s to request %s, error was: %s", openRes.Status, httpReq.URL.String(), sbody))
return nil, errors.New(sbody)
}
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}
tokenTTL, err := p.getAccessTokenTTL(ctx)
if err != nil {
return nil, err
}
url, err := url.Parse(result["app-url"].(string))
if err != nil {
return nil, err
}
urlQuery := url.Query()
if language != "" {
urlQuery.Set("ui", language) // OnlyOffice
urlQuery.Set("lang", covertLangTag(language)) // Collabora, Impact on the default document language of OnlyOffice
urlQuery.Set("UI_LLCC", language) // Office365
}
if p.conf.AppDisableChat {
urlQuery.Set("dchat", "1") // OnlyOffice disable chat
}
url.RawQuery = urlQuery.Encode()
appFullURL := url.String()
// Depending on whether wopi server returned any form parameters or not,
// we decide whether the request method is POST or GET
var formParams map[string]string
method := "GET"
if form, ok := result["form-parameters"].(map[string]interface{}); ok {
if tkn, ok := form["access_token"].(string); ok {
formParams = map[string]string{
"access_token": tkn,
"access_token_ttl": tokenTTL,
}
method = "POST"
}
}
log.Info().Msg(fmt.Sprintf("wopi: returning app URL %s", appFullURL))
return &appprovider.OpenInAppURL{
AppUrl: appFullURL,
Method: method,
FormParameters: formParams,
}, nil
}
func (p *wopiProvider) GetAppProviderInfo(ctx context.Context) (*appregistry.ProviderInfo, error) {
// Initially we store the mime types in a map to avoid duplicates
mimeTypesMap := make(map[string]bool)
for _, extensions := range p.appURLs {
for ext := range extensions {
m := mime.Detect(false, ext)
mimeTypesMap[m] = true
}
}
mimeTypes := make([]string, 0, len(mimeTypesMap))
for m := range mimeTypesMap {
mimeTypes = append(mimeTypes, m)
}
return &appregistry.ProviderInfo{
Name: p.conf.AppName,
Icon: p.conf.AppIconURI,
DesktopOnly: p.conf.AppDesktopOnly,
MimeTypes: mimeTypes,
}, nil
}
func getAppURLs(c *config) (map[string]map[string]string, error) {
// Initialize WOPI URLs by discovery
httpcl := rhttp.GetHTTPClient(
rhttp.Timeout(time.Duration(5*int64(time.Second))),
rhttp.Insecure(c.InsecureConnections),
)
appurl, err := url.Parse(c.AppIntURL)
if err != nil {
return nil, err
}
appurl.Path = path.Join(appurl.Path, "/hosting/discovery")
discReq, err := http.NewRequest("GET", appurl.String(), nil)
if err != nil {
return nil, err
}
discRes, err := httpcl.Do(discReq)
if err != nil {
return nil, err
}
defer discRes.Body.Close()
var appURLs map[string]map[string]string
switch discRes.StatusCode {
case http.StatusOK:
appURLs, err = parseWopiDiscovery(discRes.Body)
if err != nil {
return nil, errors.Wrap(err, "error parsing wopi discovery response")
}
case http.StatusNotFound:
// this may be a bridge-supported app
discReq, err = http.NewRequest("GET", c.AppIntURL, nil)
if err != nil {
return nil, err
}
discRes, err = httpcl.Do(discReq)
if err != nil {
return nil, err
}
defer discRes.Body.Close()
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(discRes.Body)
if err != nil {
return nil, err
}
// scrape app's home page to find the appname
if !strings.Contains(buf.String(), c.AppName) {
return nil, errors.New("Application server at " + c.AppURL + " does not match this AppProvider for " + c.AppName)
}
// register the supported mimetypes in the AppRegistry: this is hardcoded for the time being
// TODO(lopresti) move to config
switch c.AppName {
case "CodiMD":
appURLs = getCodimdExtensions(c.AppURL)
case "Etherpad":
appURLs = getEtherpadExtensions(c.AppURL)
default:
return nil, errors.New("Application server " + c.AppName + " running at " + c.AppURL + " is unsupported")
}
}
return appURLs, nil
}
func (p *wopiProvider) getAccessTokenTTL(ctx context.Context) (string, error) {
tkn := ctxpkg.ContextMustGetToken(ctx)
token, err := jwt.ParseWithClaims(tkn, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(p.conf.JWTSecret), nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid {
// milliseconds since Jan 1, 1970 UTC as required in https://wopi.readthedocs.io/projects/wopirest/en/latest/concepts.html?highlight=access_token_ttl#term-access-token-ttl
return strconv.FormatInt(claims.ExpiresAt.Unix()*1000, 10), nil
}
return "", errtypes.InvalidCredentials("wopi: invalid token present in ctx")
}
func parseWopiDiscovery(body io.Reader) (map[string]map[string]string, error) {
appURLs := make(map[string]map[string]string)
doc := etree.NewDocument()
if _, err := doc.ReadFrom(body); err != nil {
return nil, err
}
root := doc.SelectElement("wopi-discovery")
if root == nil {
return nil, errors.New("wopi-discovery response malformed")
}
for _, netzone := range root.SelectElements("net-zone") {
if strings.Contains(netzone.SelectAttrValue("name", ""), "external") {
for _, app := range netzone.SelectElements("app") {
for _, action := range app.SelectElements("action") {
access := action.SelectAttrValue("name", "")
if access == "view" || access == "edit" || access == "editnew" {
ext := action.SelectAttrValue("ext", "")
urlString := action.SelectAttrValue("urlsrc", "")
if ext == "" || urlString == "" {
continue
}
u, err := url.Parse(urlString)
if err != nil {
// it sucks we cannot log here because this function is run
// on init without any context.
// TODO(labkode): add logging when we'll have static logging in boot phase.
continue
}
// remove any malformed query parameter from discovery urls
q := u.Query()
for k := range q {
if strings.Contains(k, "<") || strings.Contains(k, ">") {
q.Del(k)
}
}
u.RawQuery = q.Encode()
if _, ok := appURLs[access]; !ok {
appURLs[access] = make(map[string]string)
}
appURLs[access]["."+ext] = u.String()
}
}
}
}
}
return appURLs, nil
}
func getCodimdExtensions(appURL string) map[string]map[string]string {
// Register custom mime types
mime.RegisterMime(".zmd", "application/compressed-markdown")
appURLs := make(map[string]map[string]string)
appURLs["edit"] = map[string]string{
".txt": appURL,
".md": appURL,
".zmd": appURL,
}
return appURLs
}
func getEtherpadExtensions(appURL string) map[string]map[string]string {
appURLs := make(map[string]map[string]string)
appURLs["edit"] = map[string]string{
".epd": appURL,
}
return appURLs
}
// TODO Find better solution
// This conversion was made because no other way to set the default document language to OnlyOffice was found.
func covertLangTag(lang string) string {
switch lang {
case "cs":
return "cs-CZ"
case "de":
return "de-DE"
case "es":
return "es-ES"
case "fr":
return "fr-FR"
case "it":
return "it-IT"
default:
return "en"
}
}
@@ -0,0 +1,26 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core app registry drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/app/registry/micro"
_ "github.com/opencloud-eu/reva/v2/pkg/app/registry/static"
// Add your own here
)
@@ -0,0 +1,40 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package micro
import "github.com/mitchellh/mapstructure"
type config struct {
Namespace string `mapstructure:"namespace"`
MimeTypes []*mimeTypeConfig `mapstructure:"mime_types"`
}
func (c *config) init() {
if c.Namespace == "" {
c.Namespace = "cs3"
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
@@ -0,0 +1,265 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package micro
import (
"context"
"sort"
"strconv"
"sync"
"time"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
oreg "github.com/qsfera/server/pkg/registry"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/rs/zerolog/log"
mreg "go-micro.dev/v4/registry"
)
type manager struct {
namespace string
sync.RWMutex
cancelFunc context.CancelFunc
mimeTypes map[string][]*registrypb.ProviderInfo
providers []*registrypb.ProviderInfo
config *config
}
// New returns an implementation of the app.Registry interface.
func New(m map[string]interface{}) (app.Registry, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
ctx, cancelFunc := context.WithCancel(context.Background())
newManager := manager{
namespace: c.Namespace,
cancelFunc: cancelFunc,
config: c,
}
err = newManager.updateProvidersFromMicroRegistry()
if err != nil {
if _, ok := err.(errtypes.NotFound); !ok {
return nil, err
}
}
t := time.NewTicker(time.Second * 30)
go func() {
for {
select {
case <-t.C:
log.Debug().Msg("app provider tick, updating local app list")
err = newManager.updateProvidersFromMicroRegistry()
if err != nil {
log.Error().Err(err).Msg("could not update the local provider cache")
continue
}
case <-ctx.Done():
log.Debug().Msg("app provider stopped")
t.Stop()
}
}
}()
return &newManager, nil
}
// AddProvider does not do anything for this registry, it is a placeholder to satisfy the interface
func (m *manager) AddProvider(ctx context.Context, p *registrypb.ProviderInfo) error {
log := appctx.GetLogger(ctx)
log.Info().Interface("provider", p).Msg("Tried to register through cs3 api, make sure the provider registers directly through go-micro")
return nil
}
// FindProvider returns all providers that can provide an app for the given mimeType
func (m *manager) FindProviders(ctx context.Context, mimeType string) ([]*registrypb.ProviderInfo, error) {
m.RLock()
defer m.RUnlock()
if len(m.mimeTypes[mimeType]) < 1 {
return nil, mreg.ErrNotFound
}
return m.mimeTypes[mimeType], nil
}
// GetDefaultProviderForMimeType returns the default provider for the given mimeType
func (m *manager) GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registrypb.ProviderInfo, error) {
m.RLock()
defer m.RUnlock()
for _, mt := range m.config.MimeTypes {
if mt.MimeType != mimeType {
continue
}
for _, p := range m.mimeTypes[mimeType] {
if p.Name == mt.DefaultApp {
return p, nil
}
}
}
return nil, mreg.ErrNotFound
}
// ListProviders lists all registered Providers
func (m *manager) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) {
return m.providers, nil
}
// ListSupportedMimeTypes lists all supported mimeTypes
func (m *manager) ListSupportedMimeTypes(ctx context.Context) ([]*registrypb.MimeTypeInfo, error) {
m.RLock()
defer m.RUnlock()
res := make([]*registrypb.MimeTypeInfo, 0, len(m.config.MimeTypes))
for _, mime := range m.config.MimeTypes {
res = append(res, &registrypb.MimeTypeInfo{
MimeType: mime.MimeType,
Ext: mime.Extension,
Name: mime.Name,
Description: mime.Description,
Icon: mime.Icon,
AppProviders: m.mimeTypes[mime.MimeType],
AllowCreation: mime.AllowCreation,
DefaultApplication: mime.DefaultApp,
})
}
return res, nil
}
// SetDefaultProviderForMimeType sets the default provider for the given mimeType
func (m *manager) SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registrypb.ProviderInfo) error {
m.Lock()
defer m.Unlock()
// NOTE: this is a dirty workaround:
for _, mt := range m.config.MimeTypes {
if mt.MimeType == mimeType {
mt.DefaultApp = p.Name
return nil
}
}
log.Info().Msgf("default provider for app is not set through the provider, but defined for the app")
return mreg.ErrNotFound
}
func (m *manager) getProvidersFromMicroRegistry(ctx context.Context) ([]*registrypb.ProviderInfo, error) {
reg := oreg.GetRegistry()
services, err := reg.GetService(m.namespace+".api.app-provider", mreg.GetContext(ctx))
if err != nil {
log.Warn().Err(err).Msg("getProvidersFromMicroRegistry")
}
if len(services) == 0 {
return nil, errtypes.NotFound("no application provider service registered")
}
if len(services) > 1 {
return nil, errtypes.InternalError("more than one application provider services registered")
}
providers := make([]*registrypb.ProviderInfo, 0, len(services[0].Nodes))
for _, node := range services[0].Nodes {
p := m.providerFromMetadata(node.Metadata)
p.Address = node.Address
providers = append(providers, p)
}
return providers, nil
}
func (m *manager) providerFromMetadata(metadata map[string]string) *registrypb.ProviderInfo {
p := &registrypb.ProviderInfo{
MimeTypes: splitMimeTypes(metadata[m.namespace+".app-provider.mime_type"]),
// Address: node.Address,
Name: metadata[m.namespace+".app-provider.name"],
Description: metadata[m.namespace+".app-provider.description"],
Icon: metadata[m.namespace+".app-provider.icon"],
DesktopOnly: metadata[m.namespace+".app-provider.desktop_only"] == "true",
Capability: registrypb.ProviderInfo_Capability(registrypb.ProviderInfo_Capability_value[metadata[m.namespace+".app-provider.capability"]]),
}
if metadata[m.namespace+".app-provider.priority"] != "" {
p.Opaque = &typesv1beta1.Opaque{Map: map[string]*typesv1beta1.OpaqueEntry{
"priority": {
Decoder: "plain",
Value: []byte(metadata[m.namespace+".app-provider.priority"]),
},
}}
}
return p
}
func (m *manager) updateProvidersFromMicroRegistry() error {
lst, err := m.getProvidersFromMicroRegistry(context.Background())
ma := map[string][]*registrypb.ProviderInfo{}
if err != nil {
return err
}
sortByPriority(lst)
for _, outer := range lst {
for _, inner := range outer.MimeTypes {
ma[inner] = append(ma[inner], outer)
}
}
m.Lock()
defer m.Unlock()
m.mimeTypes = ma
m.providers = lst
return nil
}
func equalsProviderInfo(p1, p2 *registrypb.ProviderInfo) bool {
sameName := p1.Name == p2.Name
sameAddress := p1.Address == p2.Address
if sameName && sameAddress {
return true
}
return false
}
func getPriority(p *registrypb.ProviderInfo) string {
if p.Opaque != nil && len(p.Opaque.Map) != 0 {
if priority, ok := p.Opaque.Map["priority"]; ok {
return string(priority.GetValue())
}
}
return defaultPriority
}
func sortByPriority(providers []*registrypb.ProviderInfo) {
less := func(i, j int) bool {
prioI, _ := strconv.ParseInt(getPriority(providers[i]), 10, 64)
prioJ, _ := strconv.ParseInt(getPriority(providers[j]), 10, 64)
return prioI < prioJ
}
sort.Slice(providers, less)
}
@@ -0,0 +1,50 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package micro
import (
"strings"
"github.com/opencloud-eu/reva/v2/pkg/app/registry/registry"
)
const defaultPriority = "0"
func init() {
registry.Register("micro", New)
}
type mimeTypeConfig struct {
MimeType string `mapstructure:"mime_type"`
Extension string `mapstructure:"extension"`
Name string `mapstructure:"name"`
Description string `mapstructure:"description"`
Icon string `mapstructure:"icon"`
DefaultApp string `mapstructure:"default_app"`
AllowCreation bool `mapstructure:"allow_creation"`
}
// use the UTF-8 record separator
func splitMimeTypes(s string) []string {
return strings.Split(s, "␞")
}
func joinMimeTypes(mimetypes []string) string {
return strings.Join(mimetypes, "␞")
}
@@ -0,0 +1,34 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import "github.com/opencloud-eu/reva/v2/pkg/app"
// NewFunc is the function that app provider implementations
// should register to at init time.
type NewFunc func(map[string]interface{}) (app.Registry, error)
// NewFuncs is a map containing all the registered app registry backends.
var NewFuncs = map[string]NewFunc{}
// Register registers a new app registry new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
@@ -0,0 +1,377 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package static
import (
"container/heap"
"context"
"fmt"
"strconv"
"strings"
"sync"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/rs/zerolog/log"
orderedmap "github.com/wk8/go-ordered-map"
)
func init() {
registry.Register("static", New)
}
const defaultPriority = 0
type mimeTypeConfig struct {
MimeType string `mapstructure:"mime_type"`
Extension string `mapstructure:"extension"`
Name string `mapstructure:"name"`
Description string `mapstructure:"description"`
Icon string `mapstructure:"icon"`
DefaultApp string `mapstructure:"default_app"`
AllowCreation bool `mapstructure:"allow_creation"`
apps providerHeap
}
type config struct {
Providers []*registrypb.ProviderInfo `mapstructure:"providers"`
MimeTypes []*mimeTypeConfig `mapstructure:"mime_types"`
}
func (c *config) init() {
if len(c.Providers) == 0 {
c.Providers = []*registrypb.ProviderInfo{}
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
type manager struct {
providers map[string]*registrypb.ProviderInfo
mimetypes *orderedmap.OrderedMap // map[string]*mimeTypeConfig -> map the mime type to the addresses of the corresponding providers
sync.RWMutex
}
// New returns an implementation of the app.Registry interface.
func New(m map[string]interface{}) (app.Registry, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
mimetypes := orderedmap.New()
for _, mime := range c.MimeTypes {
mimetypes.Set(mime.MimeType, mime)
}
providerMap := make(map[string]*registrypb.ProviderInfo)
for _, p := range c.Providers {
providerMap[p.Address] = p
}
// register providers configured manually from the config
// (different from the others that are registering themselves -
// dinamically added invoking the AddProvider function)
for _, p := range c.Providers {
if p != nil {
for _, m := range p.MimeTypes {
if v, ok := mimetypes.Get(m); ok {
mtc := v.(*mimeTypeConfig)
registerProvider(p, mtc)
} else {
return nil, errtypes.NotFound(fmt.Sprintf("mimetype %s not found in the configuration", m))
}
}
}
}
newManager := manager{
providers: providerMap,
mimetypes: mimetypes,
}
return &newManager, nil
}
// remove a provider from the provider list in a mime type
// it's a no-op if the provider is not in the list of providers in the mime type
func unregisterProvider(p *registrypb.ProviderInfo, mime *mimeTypeConfig) {
if index, in := getIndex(mime.apps, p); in {
// remove the provider from the list
heap.Remove(&mime.apps, index)
}
}
func registerProvider(p *registrypb.ProviderInfo, mime *mimeTypeConfig) {
// the app provider could be previously registered to the same mime type list
// so we will remove it
unregisterProvider(p, mime)
heap.Push(&mime.apps, providerWithPriority{
provider: p,
priority: getPriority(p),
})
}
func getPriority(p *registrypb.ProviderInfo) uint64 {
if p.Opaque != nil && len(p.Opaque.Map) != 0 {
if priority, ok := p.Opaque.Map["priority"]; ok {
if pr, err := strconv.ParseUint(string(priority.GetValue()), 10, 64); err == nil {
return pr
}
}
}
return defaultPriority
}
func (m *manager) FindProviders(ctx context.Context, mimeType string) ([]*registrypb.ProviderInfo, error) {
// find longest match
var match string
m.RLock()
defer m.RUnlock()
for pair := m.mimetypes.Oldest(); pair != nil; pair = pair.Next() {
prefix := pair.Key.(string)
if strings.HasPrefix(mimeType, prefix) && len(prefix) > len(match) {
match = prefix
}
}
if match == "" {
return nil, errtypes.NotFound("application provider not found for mime type " + mimeType)
}
mimeInterface, _ := m.mimetypes.Get(match)
mimeMatch := mimeInterface.(*mimeTypeConfig)
var providers = make([]*registrypb.ProviderInfo, 0, len(mimeMatch.apps))
for _, p := range mimeMatch.apps {
providers = append(providers, m.providers[p.provider.Address])
}
return providers, nil
}
func (m *manager) AddProvider(ctx context.Context, p *registrypb.ProviderInfo) error {
m.Lock()
defer m.Unlock()
// check if the provider was already registered
// if it's the case, we have to unregister it
// from all the old mime types
if oldP, ok := m.providers[p.Address]; ok {
oldMimeTypes := oldP.MimeTypes
for _, mimeName := range oldMimeTypes {
mimeIf, ok := m.mimetypes.Get(mimeName)
if !ok {
continue
}
mime := mimeIf.(*mimeTypeConfig)
unregisterProvider(p, mime)
}
}
m.providers[p.Address] = p
for _, mime := range p.MimeTypes {
if mimeTypeInterface, ok := m.mimetypes.Get(mime); ok {
mimeType := mimeTypeInterface.(*mimeTypeConfig)
registerProvider(p, mimeType)
} else {
// the mime type should be already registered as config in the AppRegistry
// we will create a new entry fot the mimetype, but leaving a warning for
// future log inspection for weird behaviour
// log.Warn().Msgf("config for mimetype '%s' not found while adding a new AppProvider", m)
m.mimetypes.Set(mime, dummyMimeType(mime, []*registrypb.ProviderInfo{p}))
}
}
return nil
}
func (m *manager) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) {
m.RLock()
defer m.RUnlock()
providers := make([]*registrypb.ProviderInfo, 0, len(m.providers))
for _, p := range m.providers {
providers = append(providers, p)
}
return providers, nil
}
func (m *manager) ListSupportedMimeTypes(ctx context.Context) ([]*registrypb.MimeTypeInfo, error) {
m.RLock()
defer m.RUnlock()
res := make([]*registrypb.MimeTypeInfo, 0, m.mimetypes.Len())
for pair := m.mimetypes.Oldest(); pair != nil; pair = pair.Next() {
mime := pair.Value.(*mimeTypeConfig)
res = append(res, &registrypb.MimeTypeInfo{
MimeType: mime.MimeType,
Ext: mime.Extension,
Name: mime.Name,
Description: mime.Description,
Icon: mime.Icon,
AppProviders: mime.apps.getOrderedProviderByPriority(),
AllowCreation: mime.AllowCreation,
DefaultApplication: mime.DefaultApp,
})
}
return res, nil
}
func (h providerHeap) getOrderedProviderByPriority() []*registrypb.ProviderInfo {
providers := make([]*registrypb.ProviderInfo, 0, h.Len())
for _, pp := range h {
providers = append(providers, pp.provider)
}
return providers
}
func getIndex(h providerHeap, s *registrypb.ProviderInfo) (int, bool) {
for i, e := range h {
if equalsProviderInfo(e.provider, s) {
return i, true
}
}
return -1, false
}
func (m *manager) SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registrypb.ProviderInfo) error {
m.Lock()
defer m.Unlock()
mimeInterface, ok := m.mimetypes.Get(mimeType)
if ok {
mime := mimeInterface.(*mimeTypeConfig)
mime.DefaultApp = p.Address
registerProvider(p, mime)
} else {
// the mime type should be already registered as config in the AppRegistry
// we will create a new entry fot the mimetype, but leaving a warning for
// future log inspection for weird behaviour
log.Warn().Msgf("config for mimetype '%s' not found while setting a new default AppProvider", mimeType)
m.mimetypes.Set(mimeType, dummyMimeType(mimeType, []*registrypb.ProviderInfo{p}))
}
return nil
}
func dummyMimeType(m string, apps []*registrypb.ProviderInfo) *mimeTypeConfig {
appsHeap := providerHeap{}
for _, p := range apps {
heap.Push(&appsHeap, providerWithPriority{
provider: p,
priority: getPriority(p),
})
}
return &mimeTypeConfig{
MimeType: m,
apps: appsHeap,
//Extension: "", // there is no meaningful general extension, so omit it
//Name: "", // there is no meaningful general name, so omit it
//Description: "", // there is no meaningful general description, so omit it
}
}
func (m *manager) GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registrypb.ProviderInfo, error) {
m.RLock()
defer m.RUnlock()
mimeInterface, ok := m.mimetypes.Get(mimeType)
if ok {
mime := mimeInterface.(*mimeTypeConfig)
// default by provider address
if p, ok := m.providers[mime.DefaultApp]; ok {
return p, nil
}
// default by provider name
for _, p := range m.providers {
if p.Name == mime.DefaultApp {
return p, nil
}
}
}
return nil, errtypes.NotFound("default application provider not set for mime type " + mimeType)
}
func equalsProviderInfo(p1, p2 *registrypb.ProviderInfo) bool {
return p1.Name == p2.Name
}
// check that all providers in the two lists are equals
func providersEquals(l1, l2 []*registrypb.ProviderInfo) bool {
if len(l1) != len(l2) {
return false
}
for i := 0; i < len(l1); i++ {
if !equalsProviderInfo(l1[i], l2[i]) {
return false
}
}
return true
}
type providerWithPriority struct {
provider *registrypb.ProviderInfo
priority uint64
}
type providerHeap []providerWithPriority
func (h providerHeap) Len() int {
return len(h)
}
func (h providerHeap) Less(i, j int) bool {
return h[i].priority > h[j].priority
}
func (h providerHeap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
func (h *providerHeap) Push(x interface{}) {
*h = append(*h, x.(providerWithPriority))
}
func (h *providerHeap) Pop() interface{} {
last := len(*h) - 1
x := (*h)[last]
*h = (*h)[:last]
return x
}