Initial QSfera import
This commit is contained in:
+45
@@ -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)
|
||||
}
|
||||
+76
@@ -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
|
||||
}
|
||||
+26
@@ -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
|
||||
)
|
||||
Generated
Vendored
+34
@@ -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
|
||||
}
|
||||
+492
@@ -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"
|
||||
}
|
||||
}
|
||||
+26
@@ -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
|
||||
)
|
||||
+40
@@ -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
|
||||
}
|
||||
+265
@@ -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, ®istrypb.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 := ®istrypb.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)
|
||||
}
|
||||
+50
@@ -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, "␞")
|
||||
}
|
||||
Generated
Vendored
+34
@@ -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
|
||||
}
|
||||
+377
@@ -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, ®istrypb.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
|
||||
}
|
||||
Reference in New Issue
Block a user