switch to go vendoring

This commit is contained in:
Michael Barz
2023-04-19 20:24:34 +02:00
parent 632fa05ef9
commit afc6ed1e41
8527 changed files with 3004916 additions and 2 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.OpenInAppRequest_ViewMode, token, language string) (*appprovider.OpenInAppURL, error)
GetAppProviderInfo(ctx context.Context) (*registry.ProviderInfo, error)
}
+76
View File
@@ -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/cs3org/reva/v2/pkg/app"
"github.com/cs3org/reva/v2/pkg/app/provider/registry"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/mitchellh/mapstructure"
)
func init() {
registry.Register("demo", New)
}
type demoProvider struct {
iframeUIProvider string
}
func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_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
}
+26
View File
@@ -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/cs3org/reva/v2/pkg/app/provider/demo"
_ "github.com/cs3org/reva/v2/pkg/app/provider/wopi"
// Add your own here
)
+34
View File
@@ -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/cs3org/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
}
+468
View File
@@ -0,0 +1,468 @@
// 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/cs3org/reva/v2/pkg/app"
"github.com/cs3org/reva/v2/pkg/app/provider/registry"
"github.com/cs3org/reva/v2/pkg/appctx"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/mime"
"github.com/cs3org/reva/v2/pkg/rhttp"
"github.com/cs3org/reva/v2/pkg/sharedconf"
"github.com/cs3org/reva/v2/pkg/storage/utils/templates"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/golang-jwt/jwt"
"github.com/mitchellh/mapstructure"
"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"`
}
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.OpenInAppRequest_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
}
appFullURL := result["app-url"].(string)
if language != "" {
url, err := url.Parse(appFullURL)
if err != nil {
return nil, err
}
urlQuery := url.Query()
urlQuery.Set("ui", language) // OnlyOffice
urlQuery.Set("lang", language) // Collabora
urlQuery.Set("UI_LLCC", language) // Office365
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
if discRes.StatusCode == http.StatusOK {
appURLs, err = parseWopiDiscovery(discRes.Body)
if err != nil {
return nil, errors.Wrap(err, "error parsing wopi discovery response")
}
} else if discRes.StatusCode == 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.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(p.conf.JWTSecret), nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*jwt.StandardClaims); 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*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
}
+25
View File
@@ -0,0 +1,25 @@
// 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/cs3org/reva/v2/pkg/app/registry/static"
// Add your own here
)
+34
View File
@@ -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/cs3org/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
}
+377
View File
@@ -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/cs3org/reva/v2/pkg/app"
"github.com/cs3org/reva/v2/pkg/app/registry/registry"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/mitchellh/mapstructure"
"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
}