From c39daeb0d08f4a47210ad83ebccec71f7246f800 Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Mon, 9 Jan 2023 19:34:39 +0800 Subject: [PATCH] feat(wopi): fetch discover endpoint --- assets | 2 +- pkg/wopi/discovery.go | 77 +++++++++++++++++++++++++ pkg/wopi/discovery_test.go | 25 +++++++++ pkg/wopi/types.go | 58 +++++++++++++++++++ pkg/wopi/wopi.go | 111 +++++++++++++++++++++++++++++++++++++ 5 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 pkg/wopi/discovery.go create mode 100644 pkg/wopi/discovery_test.go create mode 100644 pkg/wopi/types.go create mode 100644 pkg/wopi/wopi.go diff --git a/assets b/assets index 01343d7..d72688d 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 01343d7656f839d13cc60adc6b56a238e3160465 +Subproject commit d72688d7fceaa0bc916fc6637e928dfd5b5a1c43 diff --git a/pkg/wopi/discovery.go b/pkg/wopi/discovery.go new file mode 100644 index 0000000..21c0d92 --- /dev/null +++ b/pkg/wopi/discovery.go @@ -0,0 +1,77 @@ +package wopi + +import ( + "encoding/xml" + "fmt" + "github.com/cloudreve/Cloudreve/v3/pkg/cache" + "net/http" +) + +type ActonType string + +var ( + ActionPreview = ActonType("embedview") + ActionEdit = ActonType("edit") +) + +const ( + DiscoverResponseCacheKey = "wopi_discover" + DiscoverRefreshDuration = 24 * 3600 // 24 hrs +) + +// checkDiscovery checks if discovery content is needed to be refreshed. +// If so, it will refresh discovery content. +func (c *client) checkDiscovery() error { + c.mu.RLock() + if c.discovery == nil { + c.mu.RUnlock() + return c.refreshDiscovery() + } + + c.mu.RUnlock() + return nil +} + +// refresh Discovery action configs. +func (c *client) refreshDiscovery() error { + c.mu.Lock() + defer c.mu.Unlock() + + cached, exist := cache.Get(DiscoverResponseCacheKey) + if exist { + cachedDiscovery := cached.(WopiDiscovery) + c.discovery = &cachedDiscovery + } else { + res, err := c.http.Request("GET", c.config.discoveryEndpoint.String(), nil). + CheckHTTPResponse(http.StatusOK).GetResponse() + if err != nil { + return fmt.Errorf("failed to request discovery endpoint: %s", err) + } + + if err := xml.Unmarshal([]byte(res), &c.discovery); err != nil { + return fmt.Errorf("failed to parse response discovery endpoint: %s", err) + } + + if err := cache.Set(DiscoverResponseCacheKey, *c.discovery, DiscoverRefreshDuration); err != nil { + return err + } + } + + // construct actions map + c.actions = make(map[string]map[string]Action) + for _, app := range c.discovery.NetZone.App { + for _, action := range app.Action { + if action.Ext == "" { + continue + } + + if _, ok := c.actions["."+action.Ext]; !ok { + c.actions["."+action.Ext] = make(map[string]Action) + } + + c.actions["."+action.Ext][action.Name] = action + } + } + + return nil +} diff --git a/pkg/wopi/discovery_test.go b/pkg/wopi/discovery_test.go new file mode 100644 index 0000000..4e9bd98 --- /dev/null +++ b/pkg/wopi/discovery_test.go @@ -0,0 +1,25 @@ +package wopi + +import ( + model "github.com/cloudreve/Cloudreve/v3/models" + "github.com/cloudreve/Cloudreve/v3/pkg/cache" + "github.com/cloudreve/Cloudreve/v3/pkg/request" + "github.com/stretchr/testify/assert" + "net/url" + "testing" +) + +func TestDiscovery(t *testing.T) { + a := assert.New(t) + endpoint, _ := url.Parse("http://localhost:8001/hosting/discovery") + client := &client{ + cache: cache.Store, + http: request.NewClient(), + config: config{ + discoveryEndpoint: endpoint, + }, + } + + a.NoError(client.refreshDiscovery()) + client.NewSession(nil, &model.File{Name: "123.pptx"}, ActionPreview) +} diff --git a/pkg/wopi/types.go b/pkg/wopi/types.go new file mode 100644 index 0000000..69cf239 --- /dev/null +++ b/pkg/wopi/types.go @@ -0,0 +1,58 @@ +package wopi + +import ( + "encoding/gob" + "encoding/xml" +) + +// Response content from discovery endpoint. +type WopiDiscovery struct { + XMLName xml.Name `xml:"wopi-discovery"` + Text string `xml:",chardata"` + NetZone struct { + Text string `xml:",chardata"` + Name string `xml:"name,attr"` + App []struct { + Text string `xml:",chardata"` + Name string `xml:"name,attr"` + FavIconUrl string `xml:"favIconUrl,attr"` + BootstrapperUrl string `xml:"bootstrapperUrl,attr"` + AppBootstrapperUrl string `xml:"appBootstrapperUrl,attr"` + ApplicationBaseUrl string `xml:"applicationBaseUrl,attr"` + StaticResourceOrigin string `xml:"staticResourceOrigin,attr"` + CheckLicense string `xml:"checkLicense,attr"` + Action []Action `xml:"action"` + } `xml:"app"` + } `xml:"net-zone"` + ProofKey struct { + Text string `xml:",chardata"` + Oldvalue string `xml:"oldvalue,attr"` + Oldmodulus string `xml:"oldmodulus,attr"` + Oldexponent string `xml:"oldexponent,attr"` + Value string `xml:"value,attr"` + Modulus string `xml:"modulus,attr"` + Exponent string `xml:"exponent,attr"` + } `xml:"proof-key"` +} + +type Action struct { + Text string `xml:",chardata"` + Name string `xml:"name,attr"` + Ext string `xml:"ext,attr"` + Default string `xml:"default,attr"` + Urlsrc string `xml:"urlsrc,attr"` + Requires string `xml:"requires,attr"` + Targetext string `xml:"targetext,attr"` + Progid string `xml:"progid,attr"` + UseParent string `xml:"useParent,attr"` + Newprogid string `xml:"newprogid,attr"` + Newext string `xml:"newext,attr"` +} + +func init() { + gob.Register(WopiDiscovery{}) + gob.Register(Action{}) +} + +type Session struct { +} diff --git a/pkg/wopi/wopi.go b/pkg/wopi/wopi.go new file mode 100644 index 0000000..999dc56 --- /dev/null +++ b/pkg/wopi/wopi.go @@ -0,0 +1,111 @@ +package wopi + +import ( + "errors" + "fmt" + model "github.com/cloudreve/Cloudreve/v3/models" + "github.com/cloudreve/Cloudreve/v3/pkg/cache" + "github.com/cloudreve/Cloudreve/v3/pkg/request" + "net/url" + "path" + "strings" + "sync" +) + +type Client interface { +} + +var ( + ErrActionNotSupported = errors.New("action not supported by current wopi endpoint") + + queryPlaceholders = map[string]string{ + "BUSINESS_USER": "", + "DC_LLCC": "lng", + "DISABLE_ASYNC": "", + "DISABLE_CHAT": "", + "EMBEDDED": "true", + "FULLSCREEN": "true", + "HOST_SESSION_ID": "", + "SESSION_CONTEXT": "", + "RECORDING": "", + "THEME_ID": "darkmode", + "UI_LLCC": "lng", + "VALIDATOR_TEST_CATEGORY": "", + } +) + +const ( + wopiSrcPlaceholder = "WOPI_SOURCE" +) + +type client struct { + cache cache.Driver + http request.Client + mu sync.RWMutex + + discovery *WopiDiscovery + actions map[string]map[string]Action + + config +} + +type config struct { + discoveryEndpoint *url.URL +} + +func (c *client) NewSession(user *model.User, file *model.File, action ActonType) (*Session, error) { + if err := c.checkDiscovery(); err != nil { + return nil, err + } + + ext := path.Ext(file.Name) + availableActions, ok := c.actions[ext] + if !ok { + return nil, ErrActionNotSupported + } + + actionConfig, ok := availableActions[string(action)] + if !ok { + return nil, ErrActionNotSupported + } + + actionUrl, err := generateActionUrl(actionConfig.Urlsrc, "") + if err != nil { + return nil, err + } + + fmt.Println(actionUrl) + + return nil, nil +} + +func generateActionUrl(src string, fileSrc string) (*url.URL, error) { + src = strings.ReplaceAll(src, "<", "") + src = strings.ReplaceAll(src, ">", "") + actionUrl, err := url.Parse(src) + if err != nil { + return nil, fmt.Errorf("failed to parse action url: %s", err) + } + + queries := actionUrl.Query() + queryReplaced := url.Values{} + for k := range queries { + if placeholder, ok := queryPlaceholders[queries.Get(k)]; ok { + if placeholder != "" { + queryReplaced.Set(k, placeholder) + } + + continue + } + + if queries.Get(k) == wopiSrcPlaceholder { + queryReplaced.Set(k, fileSrc) + continue + } + + queryReplaced.Set(k, queries.Get(k)) + } + + actionUrl.RawQuery = queryReplaced.Encode() + return actionUrl, nil +}