diff --git a/changelog/unreleased/app-registry-refactor.md b/changelog/unreleased/app-registry-refactor.md
new file mode 100644
index 0000000000..bc3eaa842f
--- /dev/null
+++ b/changelog/unreleased/app-registry-refactor.md
@@ -0,0 +1,5 @@
+Enhancement: Extend app registry with AddProvider method and mimetype filters
+
+https://github.com/cs3org/reva/pull/1785
+https://github.com/cs3org/cs3apis/pull/131
+https://github.com/cs3org/reva/issues/1779
diff --git a/cmd/revad/runtime/loader.go b/cmd/revad/runtime/loader.go
index 0b90f93a6a..241f625bae 100644
--- a/cmd/revad/runtime/loader.go
+++ b/cmd/revad/runtime/loader.go
@@ -27,6 +27,8 @@ import (
_ "github.com/cs3org/reva/internal/http/interceptors/auth/tokenwriter/loader"
_ "github.com/cs3org/reva/internal/http/interceptors/loader"
_ "github.com/cs3org/reva/internal/http/services/loader"
+ _ "github.com/cs3org/reva/pkg/app/provider/loader"
+ _ "github.com/cs3org/reva/pkg/app/registry/loader"
_ "github.com/cs3org/reva/pkg/appauth/manager/loader"
_ "github.com/cs3org/reva/pkg/auth/manager/loader"
_ "github.com/cs3org/reva/pkg/auth/registry/loader"
diff --git a/examples/ocmd/ocmd-server-1.toml b/examples/ocmd/ocmd-server-1.toml
index 62b13d83a0..9d69b7ccc7 100644
--- a/examples/ocmd/ocmd-server-1.toml
+++ b/examples/ocmd/ocmd-server-1.toml
@@ -76,22 +76,18 @@ providers = "providers.demo.json"
[grpc.services.publicshareprovider]
driver = "memory"
-[grpc.services.appprovider]
-driver = "demo"
-iopsecret = "testsecret"
-wopiurl = "http://0.0.0.0:8880/"
-wopibridgeurl = "http://localhost:8000/wopib"
-
[grpc.services.appregistry]
driver = "static"
-[grpc.services.appregistry.static.rules]
-"text/plain" = "localhost:19000"
-"text/markdown" = "localhost:19000"
-"application/compressed-markdown" = "localhost:19000"
-"application/vnd.oasis.opendocument.text" = "localhost:19000"
-"application/vnd.oasis.opendocument.spreadsheet" = "localhost:19000"
-"application/vnd.oasis.opendocument.presentation" = "localhost:19000"
+[grpc.services.appprovider]
+driver = "demo"
+app_provider_url = "localhost:19000"
+
+[grpc.services.appprovider.drivers.wopi]
+iop_secret = "hello"
+wopi_url = "http://0.0.0.0:8880/"
+app_name = "Collabora"
+app_url = "https://your-collabora-server.org:9980"
[grpc.services.storageprovider]
driver = "localhome"
diff --git a/go.mod b/go.mod
index db20d8604a..792308a3ed 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
github.com/ReneKroon/ttlcache/v2 v2.7.0
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/aws/aws-sdk-go v1.40.0
+ github.com/beevik/etree v1.1.0
github.com/bluele/gcache v0.0.2
github.com/c-bata/go-prompt v0.2.5
github.com/cheggaaa/pb v1.0.29
@@ -68,6 +69,7 @@ require (
go 1.16
replace (
+ github.com/cs3org/go-cs3apis => github.com/ishank011/go-cs3apis v0.0.0-20210715114809-34729f68a479
github.com/eventials/go-tus => github.com/andrewmostello/go-tus v0.0.0-20200314041820-904a9904af9a
github.com/oleiade/reflections => github.com/oleiade/reflections v1.0.1
google.golang.org/grpc => google.golang.org/grpc v1.26.0 // temporary downgrade
diff --git a/go.sum b/go.sum
index 64681a9892..9935ce0633 100644
--- a/go.sum
+++ b/go.sum
@@ -69,6 +69,8 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
github.com/aws/aws-sdk-go v1.40.0 h1:nTCSQAeahNt15SOYxuDwJ8XvMhOU3Uqe7eJUPv7+Vsk=
github.com/aws/aws-sdk-go v1.40.0/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
+github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
+github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -104,8 +106,6 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4=
-github.com/cs3org/go-cs3apis v0.0.0-20210702091910-85a56bfd027f h1:l09QSEPO8DI3V2hBnc6KhTsrNg/DTyBYjCTwSb/HR6Q=
-github.com/cs3org/go-cs3apis v0.0.0-20210702091910-85a56bfd027f/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -206,6 +206,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -266,9 +267,13 @@ github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoP
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU=
+github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0=
+github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@@ -284,6 +289,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.0 h1:gvV6jG9dTgFEncxo+AF7PH6MZXi/vZl25owA/8Dg8Wo=
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
@@ -293,8 +300,12 @@ github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
+github.com/ishank011/go-cs3apis v0.0.0-20210715114809-34729f68a479 h1:xB9tNEbbRzAMuE2IVVq/aTi9VKUHWbfm+/f2+08w4L0=
+github.com/ishank011/go-cs3apis v0.0.0-20210715114809-34729f68a479/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
+github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
+github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
@@ -375,6 +386,8 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
@@ -408,6 +421,7 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
+github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -624,6 +638,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -814,6 +829,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
diff --git a/internal/grpc/services/appprovider/appprovider.go b/internal/grpc/services/appprovider/appprovider.go
index b7545f7927..fe2479d433 100644
--- a/internal/grpc/services/appprovider/appprovider.go
+++ b/internal/grpc/services/appprovider/appprovider.go
@@ -19,28 +19,23 @@
package appprovider
import (
- "bytes"
"context"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
"os"
- "path"
- "strings"
"time"
providerpb "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
+ registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
+ rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/cs3org/reva/pkg/app"
- "github.com/cs3org/reva/pkg/app/provider/demo"
- "github.com/cs3org/reva/pkg/appctx"
+ "github.com/cs3org/reva/pkg/app/provider/registry"
"github.com/cs3org/reva/pkg/errtypes"
+ "github.com/cs3org/reva/pkg/logger"
"github.com/cs3org/reva/pkg/rgrpc"
"github.com/cs3org/reva/pkg/rgrpc/status"
- "github.com/cs3org/reva/pkg/rhttp"
- "github.com/cs3org/reva/pkg/user"
+ "github.com/cs3org/reva/pkg/rgrpc/todo/pool"
+ "github.com/cs3org/reva/pkg/sharedconf"
"github.com/mitchellh/mapstructure"
+ "github.com/pkg/errors"
"google.golang.org/grpc"
)
@@ -50,16 +45,31 @@ func init() {
type service struct {
provider app.Provider
- client *http.Client
conf *config
}
type config struct {
- Driver string `mapstructure:"driver"`
- Demo map[string]interface{} `mapstructure:"demo"`
- IopSecret string `mapstructure:"iopsecret" docs:";The iopsecret used to connect to the wopiserver."`
- WopiURL string `mapstructure:"wopiurl" docs:";The wopiserver's URL."`
- WopiBrURL string `mapstructure:"wopibridgeurl" docs:";The wopibridge's URL."`
+ Driver string `mapstructure:"driver"`
+ Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
+ AppProviderURL string `mapstructure:"app_provider_url"`
+ GatewaySvc string `mapstructure:"gatewaysvc"`
+}
+
+func (c *config) init() {
+ if c.Driver == "" {
+ c.Driver = "demo"
+ }
+ c.AppProviderURL = sharedconf.GetGatewaySVC(c.AppProviderURL)
+ c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
+}
+
+func parseConfig(m map[string]interface{}) (*config, error) {
+ c := &config{}
+ if err := mapstructure.Decode(m, c); err != nil {
+ return nil, err
+ }
+ c.init()
+ return c, nil
}
// New creates a new AppProviderService
@@ -77,20 +87,40 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
service := &service{
conf: c,
provider: provider,
- client: rhttp.GetHTTPClient(
- rhttp.Timeout(5 * time.Second),
- ),
}
+ go service.registerProvider()
return service, nil
}
-func parseConfig(m map[string]interface{}) (*config, error) {
- c := &config{}
- if err := mapstructure.Decode(m, c); err != nil {
- return nil, err
+func (s *service) registerProvider() {
+ // Give the appregistry service time to come up
+ time.Sleep(2 * time.Second)
+
+ ctx := context.Background()
+ log := logger.New().With().Int("pid", os.Getpid()).Logger()
+ pInfo, err := s.provider.GetAppProviderInfo(ctx)
+ if err != nil {
+ log.Error().Err(err).Msgf("error registering app provider: could not get provider info")
+ return
+ }
+ pInfo.Address = s.conf.AppProviderURL
+
+ client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
+ if err != nil {
+ log.Error().Err(err).Msgf("error registering app provider: could not get gateway client")
+ return
+ }
+ res, err := client.AddAppProvider(ctx, ®istrypb.AddAppProviderRequest{Provider: pInfo})
+ if err != nil {
+ log.Error().Err(err).Msgf("error registering app provider: error calling add app provider")
+ return
+ }
+ if res.Status.Code != rpc.Code_CODE_OK {
+ err = status.NewErrorFromCode(res.Status.Code, "appprovider")
+ log.Error().Err(err).Msgf("error registering app provider: add app provider returned error")
+ return
}
- return c, nil
}
func (s *service) Close() error {
@@ -106,182 +136,27 @@ func (s *service) Register(ss *grpc.Server) {
}
func getProvider(c *config) (app.Provider, error) {
- switch c.Driver {
- case "demo":
- return demo.New(c.Demo)
- default:
- return nil, errtypes.NotFound("driver not found: " + c.Driver)
+ if f, ok := registry.NewFuncs[c.Driver]; ok {
+ return f(c.Drivers[c.Driver])
}
-}
-
-func (s *service) getWopiAppEndpoints(ctx context.Context) (map[string]interface{}, error) {
- // TODO this query will eventually be served by Reva.
- // For the time being it is a remnant of the CERNBox-specific WOPI server, which justifies the /cbox path in the URL.
- wopiurl, err := url.Parse(s.conf.WopiURL)
- if err != nil {
- return nil, err
- }
- wopiurl.Path = path.Join(wopiurl.Path, "/wopi/cbox/endpoints")
- appsReq, err := rhttp.NewRequest(ctx, "GET", wopiurl.String(), nil)
- if err != nil {
- return nil, err
- }
- appsRes, err := s.client.Do(appsReq)
- if err != nil {
- return nil, err
- }
- defer appsRes.Body.Close()
- if appsRes.StatusCode != http.StatusOK {
- return nil, errtypes.InternalError(fmt.Sprintf("Request to WOPI server returned %d", appsRes.StatusCode))
- }
- appsBody, err := ioutil.ReadAll(appsRes.Body)
- if err != nil {
- return nil, err
- }
-
- appsURLMap := make(map[string]interface{})
- err = json.Unmarshal(appsBody, &appsURLMap)
- if err != nil {
- return nil, err
- }
-
- log := appctx.GetLogger(ctx)
- log.Info().Msg(fmt.Sprintf("Successfully retrieved %d WOPI app endpoints", len(appsURLMap)))
- return appsURLMap, nil
+ return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func (s *service) OpenInApp(ctx context.Context, req *providerpb.OpenInAppRequest) (*providerpb.OpenInAppResponse, error) {
-
- log := appctx.GetLogger(ctx)
-
- wopiurl, err := url.Parse(s.conf.WopiURL)
- if err != nil {
- return nil, err
- }
- wopiurl.Path = path.Join(wopiurl.Path, "/wopi/iop/open")
- httpReq, err := rhttp.NewRequest(ctx, "GET", wopiurl.String(), nil)
+ appURL, err := s.provider.GetAppURL(ctx, req.ResourceInfo, req.ViewMode, req.AccessToken)
if err != nil {
- return nil, err
- }
-
- q := httpReq.URL.Query()
- q.Add("fileid", req.ResourceInfo.GetId().OpaqueId)
- q.Add("endpoint", req.ResourceInfo.GetId().StorageId)
- q.Add("viewmode", req.ViewMode.String())
- // TODO the folder URL should be resolved as e.g. `'https://cernbox.cern.ch/index.php/apps/files/?dir=' + filepath.Dir(req.Ref.GetPath())`
- // or should be deprecated/removed altogether, needs discussion and decision.
- q.Add("folderurl", "undefined")
- u, ok := user.ContextGetUser(ctx)
- if ok {
- q.Add("username", u.Username)
- }
- // else defaults to "Anonymous Guest"
-
- if s.conf.IopSecret == "" {
- s.conf.IopSecret = os.Getenv("REVA_APPPROVIDER_IOPSECRET")
- }
-
- httpReq.Header.Set("Authorization", "Bearer "+s.conf.IopSecret)
- httpReq.Header.Set("TokenHeader", req.AccessToken)
-
- httpReq.URL.RawQuery = q.Encode()
-
- openRes, err := s.client.Do(httpReq)
-
- if err != nil {
- res := &providerpb.OpenInAppResponse{
- Status: status.NewInternal(ctx, err, "appprovider: error performing open request to WOPI"),
- }
- return res, nil
- }
- defer openRes.Body.Close()
-
- if openRes.StatusCode != http.StatusOK {
+ err := errors.Wrap(err, "appprovider: error calling GetAppURL")
res := &providerpb.OpenInAppResponse{
- Status: status.NewInvalid(ctx, fmt.Sprintf("appprovider: error performing open request to WOPI, status code: %d", openRes.StatusCode)),
+ Status: status.NewInternal(ctx, err, "error getting app URL"),
}
return res, nil
}
-
- buf := new(bytes.Buffer)
- _, err = buf.ReadFrom(openRes.Body)
- if err != nil {
- return nil, err
- }
- openResBody := buf.String()
-
- var viewmode string
- if req.ViewMode == providerpb.OpenInAppRequest_VIEW_MODE_READ_WRITE {
- viewmode = "edit"
- } else {
- viewmode = "view"
- }
-
- var appProviderURL string
- if req.App == "" {
- // Default behavior: work out the application URL to be used for this file
- // TODO call this e.g. once a day or a week, and cache the content in a shared map protected by a multi-reader Lock
- appsURLMap, err := s.getWopiAppEndpoints(ctx)
- if err != nil {
- res := &providerpb.OpenInAppResponse{
- Status: status.NewInternal(ctx, err, "appprovider: getWopiAppEndpoints failed"),
- }
- return res, nil
- }
- viewOptions := appsURLMap[path.Ext(req.ResourceInfo.GetPath())]
- viewOptionsMap, ok := viewOptions.(map[string]interface{})
- if !ok {
- res := &providerpb.OpenInAppResponse{
- Status: status.NewInvalid(ctx, "Incorrect parsing of the App URLs map from the WOPI server"),
- }
- return res, nil
- }
-
- appProviderURL = fmt.Sprintf("%v", viewOptionsMap[viewmode])
- if strings.Contains(appProviderURL, "?") {
- appProviderURL += "&"
- } else {
- appProviderURL += "?"
- }
- appProviderURL = fmt.Sprintf("%sWOPISrc=%s", appProviderURL, openResBody)
- } else {
- // User specified the application to use, generate the URL out of that
- // TODO map the given req.App to the URL via config. For now assume it's a URL!
- appProviderURL = fmt.Sprintf("%sWOPISrc=%s", req.App, openResBody)
- }
-
- // In case of applications served by the WOPI bridge, resolve the URL and go to the app
- // Note that URL matching is performed via string matching, not via IP resolution: may need to fix this
- if len(s.conf.WopiBrURL) > 0 && strings.Contains(appProviderURL, s.conf.WopiBrURL) {
- httpClient := rhttp.GetHTTPClient(
- rhttp.Context(ctx),
- rhttp.Timeout(time.Duration(5*int64(time.Second))),
- )
- httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
- // do not follow a redirect
- return http.ErrUseLastResponse
- }
-
- bridgeReq, err := rhttp.NewRequest(ctx, "GET", appProviderURL, nil)
- if err != nil {
- return nil, err
- }
- bridgeRes, err := httpClient.Do(bridgeReq)
- if err != nil {
- return nil, err
- }
- defer bridgeRes.Body.Close()
- if bridgeRes.StatusCode != http.StatusFound {
- return nil, errtypes.InternalError(fmt.Sprintf("Request to WOPI bridge returned %d", bridgeRes.StatusCode))
- }
- appProviderURL = bridgeRes.Header.Get("Location")
+ res := &providerpb.OpenInAppResponse{
+ Status: status.NewOK(ctx),
+ AppUrl: appURL,
}
+ return res, nil
- log.Info().Msg(fmt.Sprintf("Returning app provider URL %s", appProviderURL))
- return &providerpb.OpenInAppResponse{
- Status: status.NewOK(ctx),
- AppUrl: appProviderURL,
- }, nil
}
func (s *service) OpenFileInAppProvider(ctx context.Context, req *providerpb.OpenFileInAppProviderRequest) (*providerpb.OpenFileInAppProviderResponse, error) {
diff --git a/internal/grpc/services/appprovider/appprovider_test.go b/internal/grpc/services/appprovider/appprovider_test.go
index 14b83d0d7c..4d76286565 100644
--- a/internal/grpc/services/appprovider/appprovider_test.go
+++ b/internal/grpc/services/appprovider/appprovider_test.go
@@ -34,29 +34,36 @@ func Test_parseConfig(t *testing.T) {
wantErr interface{}
}{
{
- name: "all configurations set",
+ name: "all configurations set for demo driver",
m: map[string]interface{}{
- "Driver": "demo",
- "Demo": map[string]interface{}{"a": "b", "c": "d"},
- "IopSecret": "very-secret",
- "WopiURL": "https://my.wopi:9871",
+ "Driver": "demo",
+ "Drivers": map[string]map[string]interface{}{"demo": map[string]interface{}{"a": "b", "c": "d"}},
},
want: &config{
- Driver: "demo",
- Demo: map[string]interface{}{"a": "b", "c": "d"},
- IopSecret: "very-secret",
- WopiURL: "https://my.wopi:9871",
+ Driver: "demo",
+ Drivers: map[string]map[string]interface{}{"demo": map[string]interface{}{"a": "b", "c": "d"}},
+ },
+ wantErr: nil,
+ },
+ {
+ name: "all configurations set for wopi driver",
+ m: map[string]interface{}{
+ "Driver": "wopi",
+ "Drivers": map[string]map[string]interface{}{"wopi": map[string]interface{}{"iop_secret": "very-secret", "wopi_url": "https://my.wopi:9871"}},
+ },
+ want: &config{
+ Driver: "wopi",
+ Drivers: map[string]map[string]interface{}{"wopi": map[string]interface{}{"iop_secret": "very-secret", "wopi_url": "https://my.wopi:9871"}},
},
wantErr: nil,
},
{
name: "wrong type of setting",
- m: map[string]interface{}{"Driver": 123, "IopSecret": 456},
+ m: map[string]interface{}{"Driver": 123, "NonExistentField": 456},
want: nil,
wantErr: &mapstructure.Error{
Errors: []string{
"'driver' expected type 'string', got unconvertible type 'int', value: '123'",
- "'iopsecret' expected type 'string', got unconvertible type 'int', value: '456'",
},
},
},
@@ -64,10 +71,8 @@ func Test_parseConfig(t *testing.T) {
name: "undefined settings type",
m: map[string]interface{}{"Not-Defined": 123},
want: &config{
- Driver: "",
- Demo: map[string]interface{}(nil),
- IopSecret: "",
- WopiURL: "",
+ Driver: "demo",
+ Drivers: map[string]map[string]interface{}(nil),
},
wantErr: nil,
},
diff --git a/internal/grpc/services/appregistry/appregistry.go b/internal/grpc/services/appregistry/appregistry.go
index 3f2f99db6c..5fa7ed6f76 100644
--- a/internal/grpc/services/appregistry/appregistry.go
+++ b/internal/grpc/services/appregistry/appregistry.go
@@ -25,7 +25,7 @@ import (
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/cs3org/reva/pkg/app"
- "github.com/cs3org/reva/pkg/app/registry/static"
+ "github.com/cs3org/reva/pkg/app/registry/registry"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/rgrpc"
"github.com/cs3org/reva/pkg/rgrpc/status"
@@ -37,7 +37,7 @@ func init() {
}
type svc struct {
- registry app.Registry
+ reg app.Registry
}
func (s *svc) Close() error {
@@ -45,7 +45,7 @@ func (s *svc) Close() error {
}
func (s *svc) UnprotectedEndpoints() []string {
- return []string{}
+ return []string{"/cs3.app.registry.v1beta1.RegistryAPI/AddAppProvider"}
}
func (s *svc) Register(ss *grpc.Server) {
@@ -53,8 +53,14 @@ func (s *svc) Register(ss *grpc.Server) {
}
type config struct {
- Driver string `mapstructure:"driver"`
- Static map[string]interface{} `mapstructure:"static"`
+ Driver string `mapstructure:"driver"`
+ Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
+}
+
+func (c *config) init() {
+ if c.Driver == "" {
+ c.Driver = "static"
+ }
}
// New creates a new StorageRegistryService
@@ -65,13 +71,13 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
return nil, err
}
- registry, err := getRegistry(c)
+ reg, err := getRegistry(c)
if err != nil {
return nil, err
}
svc := &svc{
- registry: registry,
+ reg: reg,
}
return svc, nil
@@ -82,45 +88,53 @@ func parseConfig(m map[string]interface{}) (*config, error) {
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
+ c.init()
return c, nil
}
func getRegistry(c *config) (app.Registry, error) {
- switch c.Driver {
- case "static":
- return static.New(c.Static)
- default:
- return nil, errtypes.NotFound("driver not found: " + c.Driver)
+ if f, ok := registry.NewFuncs[c.Driver]; ok {
+ return f(c.Drivers[c.Driver])
}
+ return nil, errtypes.NotFound("appregistrysvc: driver not found: " + c.Driver)
}
func (s *svc) GetAppProviders(ctx context.Context, req *registrypb.GetAppProvidersRequest) (*registrypb.GetAppProvidersResponse, error) {
- p, err := s.registry.FindProvider(ctx, req.ResourceInfo.MimeType)
+ p, err := s.reg.FindProviders(ctx, req.ResourceInfo.MimeType)
if err != nil {
return ®istrypb.GetAppProvidersResponse{
Status: status.NewInternal(ctx, err, "error looking for the app provider"),
}, nil
}
- provider := format(p)
res := ®istrypb.GetAppProvidersResponse{
Status: status.NewOK(ctx),
- Providers: []*registrypb.ProviderInfo{provider},
+ Providers: p,
+ }
+ return res, nil
+}
+
+func (s *svc) AddAppProvider(ctx context.Context, req *registrypb.AddAppProviderRequest) (*registrypb.AddAppProviderResponse, error) {
+ err := s.reg.AddProvider(ctx, req.Provider)
+ if err != nil {
+ return ®istrypb.AddAppProviderResponse{
+ Status: status.NewInternal(ctx, err, "error adding the app provider"),
+ }, nil
+ }
+
+ res := ®istrypb.AddAppProviderResponse{
+ Status: status.NewOK(ctx),
}
return res, nil
}
func (s *svc) ListAppProviders(ctx context.Context, req *registrypb.ListAppProvidersRequest) (*registrypb.ListAppProvidersResponse, error) {
- pvds, err := s.registry.ListProviders(ctx)
+ providers, err := s.reg.ListProviders(ctx)
if err != nil {
return ®istrypb.ListAppProvidersResponse{
Status: status.NewInternal(ctx, err, "error listing the app providers"),
}, nil
}
- providers := make([]*registrypb.ProviderInfo, 0, len(pvds))
- for _, pvd := range pvds {
- providers = append(providers, format(pvd))
- }
res := ®istrypb.ListAppProvidersResponse{
Status: status.NewOK(ctx),
@@ -129,8 +143,31 @@ func (s *svc) ListAppProviders(ctx context.Context, req *registrypb.ListAppProvi
return res, nil
}
-func format(p *app.ProviderInfo) *registrypb.ProviderInfo {
- return ®istrypb.ProviderInfo{
- Address: p.Location,
+func (s *svc) GetDefaultAppProviderForMimeType(ctx context.Context, req *registrypb.GetDefaultAppProviderForMimeTypeRequest) (*registrypb.GetDefaultAppProviderForMimeTypeResponse, error) {
+ provider, err := s.reg.GetDefaultProviderForMimeType(ctx, req.MimeType)
+ if err != nil {
+ return ®istrypb.GetDefaultAppProviderForMimeTypeResponse{
+ Status: status.NewInternal(ctx, err, "error getting the default app provider for the mimetype"),
+ }, nil
+ }
+
+ res := ®istrypb.GetDefaultAppProviderForMimeTypeResponse{
+ Status: status.NewOK(ctx),
+ Provider: provider,
+ }
+ return res, nil
+}
+
+func (s *svc) SetDefaultAppProviderForMimeType(ctx context.Context, req *registrypb.SetDefaultAppProviderForMimeTypeRequest) (*registrypb.SetDefaultAppProviderForMimeTypeResponse, error) {
+ err := s.reg.SetDefaultProviderForMimeType(ctx, req.MimeType, req.Provider)
+ if err != nil {
+ return ®istrypb.SetDefaultAppProviderForMimeTypeResponse{
+ Status: status.NewInternal(ctx, err, "error setting the default app provider for the mimetype"),
+ }, nil
}
+
+ res := ®istrypb.SetDefaultAppProviderForMimeTypeResponse{
+ Status: status.NewOK(ctx),
+ }
+ return res, nil
}
diff --git a/internal/grpc/services/appregistry/appregistry_test.go b/internal/grpc/services/appregistry/appregistry_test.go
index b2c7cb4e9c..684a1c3ce6 100644
--- a/internal/grpc/services/appregistry/appregistry_test.go
+++ b/internal/grpc/services/appregistry/appregistry_test.go
@@ -38,15 +38,21 @@ func (a ByAddress) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func Test_ListAppProviders(t *testing.T) {
tests := []struct {
- name string
- rules map[string]interface{}
- want *registrypb.ListAppProvidersResponse
+ name string
+ providers map[string]interface{}
+ want *registrypb.ListAppProvidersResponse
}{
{
name: "simple test",
- rules: map[string]interface{}{
- "text/json": "some Address",
- "currently/ignored": "an other address",
+ providers: map[string]interface{}{
+ "some Address": map[string]interface{}{
+ "address": "some Address",
+ "mimetypes": []string{"text/json"},
+ },
+ "another address": map[string]interface{}{
+ "address": "another address",
+ "mimetypes": []string{"currently/ignored"},
+ },
},
// only Status and Providers will be asserted in the tests
@@ -57,27 +63,36 @@ func Test_ListAppProviders(t *testing.T) {
Message: "",
},
Providers: []*registrypb.ProviderInfo{
- {Address: "some Address"},
- {Address: "an other address"},
+ {
+ Address: "some Address",
+ MimeTypes: []string{"text/json"},
+ },
+ {
+ Address: "another address",
+ MimeTypes: []string{"currently/ignored"},
+ },
},
},
},
{
- name: "rules is nil",
- rules: nil,
+ name: "providers is nil",
+ providers: nil,
want: ®istrypb.ListAppProvidersResponse{
Status: &rpcv1beta1.Status{
Code: 1,
Trace: "00000000000000000000000000000000",
},
Providers: []*registrypb.ProviderInfo{
- {Address: ""},
+ {
+ Address: "",
+ MimeTypes: []string{"text/plain"},
+ },
},
},
},
{
- name: "empty rules",
- rules: map[string]interface{}{},
+ name: "empty providers",
+ providers: map[string]interface{}{},
// only Status and Providers will be asserted in the tests
want: ®istrypb.ListAppProvidersResponse{
@@ -87,14 +102,17 @@ func Test_ListAppProviders(t *testing.T) {
Message: "",
},
Providers: []*registrypb.ProviderInfo{
- {Address: ""},
+ {
+ Address: "",
+ MimeTypes: []string{"text/plain"},
+ },
},
},
},
{
- name: "rule value is nil",
- rules: map[string]interface{}{
- "text/json": nil,
+ name: "provider value is nil",
+ providers: map[string]interface{}{
+ "some Address": nil,
},
// only Status and Providers will be asserted in the tests
@@ -104,23 +122,21 @@ func Test_ListAppProviders(t *testing.T) {
Trace: "00000000000000000000000000000000",
Message: "",
},
- Providers: []*registrypb.ProviderInfo{
- {Address: ""},
- },
+ Providers: []*registrypb.ProviderInfo{nil},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- rr, err := static.New(map[string]interface{}{"Rules": tt.rules})
+ rr, err := static.New(map[string]interface{}{"Providers": tt.providers})
if err != nil {
t.Errorf("could not create registry error = %v", err)
return
}
ss := &svc{
- registry: rr,
+ reg: rr,
}
got, err := ss.ListAppProviders(context.Background(), nil)
@@ -137,13 +153,19 @@ func Test_ListAppProviders(t *testing.T) {
}
func Test_GetAppProviders(t *testing.T) {
- rules := map[string]interface{}{
- "text/json": "JSON format",
- "image/bmp": "Windows OS/2 Bitmap Graphics",
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "Microsoft Word (OpenXML)",
- "application/vnd.oasis.opendocument.presentation": "OpenDocument presentation document",
- "application/vnd.apple.installer+xml": "Apple Installer Package",
- "text/xml": "XML",
+ providers := map[string]interface{}{
+ "text appprovider addr": map[string]interface{}{
+ "address": "text appprovider addr",
+ "mimetypes": []string{"text/json", "text/xml"},
+ },
+ "image appprovider addr": map[string]interface{}{
+ "address": "image appprovider addr",
+ "mimetypes": []string{"image/bmp"},
+ },
+ "misc appprovider addr": map[string]interface{}{
+ "address": "misc appprovider addr",
+ "mimetypes": []string{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.oasis.opendocument.presentation", "application/vnd.apple.installer+xml"},
+ },
}
tests := []struct {
@@ -162,7 +184,10 @@ func Test_GetAppProviders(t *testing.T) {
Message: "",
},
Providers: []*registrypb.ProviderInfo{
- {Address: "JSON format"},
+ {
+ Address: "text appprovider addr",
+ MimeTypes: []string{"text/json", "text/xml"},
+ },
},
},
},
@@ -176,7 +201,10 @@ func Test_GetAppProviders(t *testing.T) {
Message: "",
},
Providers: []*registrypb.ProviderInfo{
- {Address: "Apple Installer Package"},
+ {
+ Address: "misc appprovider addr",
+ MimeTypes: []string{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.oasis.opendocument.presentation", "application/vnd.apple.installer+xml"},
+ },
},
},
},
@@ -230,14 +258,14 @@ func Test_GetAppProviders(t *testing.T) {
},
}
- rr, err := static.New(map[string]interface{}{"Rules": rules})
+ rr, err := static.New(map[string]interface{}{"providers": providers})
if err != nil {
t.Errorf("could not create registry error = %v", err)
return
}
ss := &svc{
- registry: rr,
+ reg: rr,
}
for _, tt := range tests {
@@ -260,11 +288,11 @@ func Test_GetAppProviders(t *testing.T) {
func TestNew(t *testing.T) {
tests := []struct {
- name string
- m map[string]interface{}
- rules map[string]interface{}
- want svc
- wantErr interface{}
+ name string
+ m map[string]interface{}
+ providers map[string]interface{}
+ want svc
+ wantErr interface{}
}{
{
name: "no error",
@@ -274,12 +302,12 @@ func TestNew(t *testing.T) {
{
name: "not existing driver",
m: map[string]interface{}{"Driver": "doesnotexist"},
- wantErr: "error: not found: driver not found: doesnotexist",
+ wantErr: "error: not found: appregistrysvc: driver not found: doesnotexist",
},
{
name: "empty",
m: map[string]interface{}{},
- wantErr: "error: not found: driver not found: ",
+ wantErr: nil,
},
{
name: "extra not existing field in setting",
diff --git a/internal/grpc/services/gateway/appprovider.go b/internal/grpc/services/gateway/appprovider.go
index f7921c9520..304e36f038 100644
--- a/internal/grpc/services/gateway/appprovider.go
+++ b/internal/grpc/services/gateway/appprovider.go
@@ -183,7 +183,7 @@ func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.Resour
}, nil
}
- provider, err := s.findAppProvider(ctx, ri)
+ provider, err := s.findAppProvider(ctx, ri, app)
if err != nil {
err = errors.Wrap(err, "gateway: error calling findAppProvider")
var st *rpc.Status
@@ -208,7 +208,6 @@ func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.Resour
appProviderReq := &providerpb.OpenInAppRequest{
ResourceInfo: ri,
ViewMode: providerpb.OpenInAppRequest_ViewMode(vm),
- App: app,
AccessToken: accessToken,
}
@@ -220,32 +219,49 @@ func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.Resour
return res, nil
}
-func (s *svc) findAppProvider(ctx context.Context, ri *storageprovider.ResourceInfo) (*registry.ProviderInfo, error) {
+func (s *svc) findAppProvider(ctx context.Context, ri *storageprovider.ResourceInfo, app string) (*registry.ProviderInfo, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
err = errors.Wrap(err, "gateway: error getting appregistry client")
return nil, err
}
+
+ if app == "" {
+ // We need to get the default provider in case app is not set
+ // If the default isn't set as well, we'll return the first provider which matches the mimetype
+ res, err := c.GetDefaultAppProviderForMimeType(ctx, ®istry.GetDefaultAppProviderForMimeTypeRequest{
+ MimeType: ri.MimeType,
+ })
+ if err == nil && res.Status.Code == rpc.Code_CODE_OK && res.Provider != nil {
+ return res.Provider, nil
+ }
+ }
+
res, err := c.GetAppProviders(ctx, ®istry.GetAppProvidersRequest{
ResourceInfo: ri,
})
-
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetAppProviders")
return nil, err
}
-
- // TODO(labkode): when sending an Open to the proxy we need to choose one
- // provider from the list of available as the client
- if res.Status.Code == rpc.Code_CODE_OK {
- return res.Providers[0], nil
+ if res.Status.Code != rpc.Code_CODE_OK {
+ if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
+ return nil, errtypes.NotFound("gateway: app provider not found for resource: " + ri.String())
+ }
+ return nil, errtypes.InternalError("gateway: error finding app providers")
}
- if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
- return nil, errtypes.NotFound("gateway: app provider not found for resource: " + ri.String())
+ if app != "" {
+ for _, p := range res.Providers {
+ if p.Name == app {
+ return p, nil
+ }
+ }
+ return nil, errtypes.NotFound("gateway: app provider not found: " + app)
}
- return nil, errtypes.InternalError("gateway: error finding a storage provider")
+ // As a fallback, return the first provider in the list
+ return res.Providers[0], nil
}
func getGRPCConfig(opaque *typespb.Opaque) (bool, bool) {
diff --git a/internal/grpc/services/gateway/appregistry.go b/internal/grpc/services/gateway/appregistry.go
index a02f18f9de..cf50c14079 100644
--- a/internal/grpc/services/gateway/appregistry.go
+++ b/internal/grpc/services/gateway/appregistry.go
@@ -32,13 +32,30 @@ func (s *svc) GetAppProviders(ctx context.Context, req *registry.GetAppProviders
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient")
return ®istry.GetAppProvidersResponse{
- Status: status.NewInternal(ctx, err, "error getting user share provider client"),
+ Status: status.NewInternal(ctx, err, "error getting app registry client"),
}, nil
}
res, err := c.GetAppProviders(ctx, req)
if err != nil {
- return nil, errors.Wrap(err, "gateway: error calling ListShares")
+ return nil, errors.Wrap(err, "gateway: error calling GetAppProviders")
+ }
+
+ return res, nil
+}
+
+func (s *svc) AddAppProvider(ctx context.Context, req *registry.AddAppProviderRequest) (*registry.AddAppProviderResponse, error) {
+ c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
+ if err != nil {
+ err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient")
+ return ®istry.AddAppProviderResponse{
+ Status: status.NewInternal(ctx, err, "error getting app registry client"),
+ }, nil
+ }
+
+ res, err := c.AddAppProvider(ctx, req)
+ if err != nil {
+ return nil, errors.Wrap(err, "gateway: error calling AddAppProvider")
}
return res, nil
@@ -49,13 +66,47 @@ func (s *svc) ListAppProviders(ctx context.Context, req *registry.ListAppProvide
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient")
return ®istry.ListAppProvidersResponse{
- Status: status.NewInternal(ctx, err, "error getting user share provider client"),
+ Status: status.NewInternal(ctx, err, "error getting app registry client"),
}, nil
}
res, err := c.ListAppProviders(ctx, req)
if err != nil {
- return nil, errors.Wrap(err, "gateway: error calling ListShares")
+ return nil, errors.Wrap(err, "gateway: error calling ListAppProviders")
+ }
+
+ return res, nil
+}
+
+func (s *svc) GetDefaultAppProviderForMimeType(ctx context.Context, req *registry.GetDefaultAppProviderForMimeTypeRequest) (*registry.GetDefaultAppProviderForMimeTypeResponse, error) {
+ c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
+ if err != nil {
+ err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient")
+ return ®istry.GetDefaultAppProviderForMimeTypeResponse{
+ Status: status.NewInternal(ctx, err, "error getting app registry client"),
+ }, nil
+ }
+
+ res, err := c.GetDefaultAppProviderForMimeType(ctx, req)
+ if err != nil {
+ return nil, errors.Wrap(err, "gateway: error calling GetDefaultAppProviderForMimeType")
+ }
+
+ return res, nil
+}
+
+func (s *svc) SetDefaultAppProviderForMimeType(ctx context.Context, req *registry.SetDefaultAppProviderForMimeTypeRequest) (*registry.SetDefaultAppProviderForMimeTypeResponse, error) {
+ c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
+ if err != nil {
+ err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient")
+ return ®istry.SetDefaultAppProviderForMimeTypeResponse{
+ Status: status.NewInternal(ctx, err, "error getting app registry client"),
+ }, nil
+ }
+
+ res, err := c.SetDefaultAppProviderForMimeType(ctx, req)
+ if err != nil {
+ return nil, errors.Wrap(err, "gateway: error calling SetDefaultAppProviderForMimeType")
}
return res, nil
diff --git a/internal/http/services/appprovider/appprovider.go b/internal/http/services/appprovider/appprovider.go
new file mode 100644
index 0000000000..bae42ca2c1
--- /dev/null
+++ b/internal/http/services/appprovider/appprovider.go
@@ -0,0 +1,232 @@
+// 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 ocmd
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
+ rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
+ provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+ "github.com/cs3org/reva/internal/http/services/ocmd"
+ "github.com/cs3org/reva/pkg/rgrpc/status"
+ "github.com/cs3org/reva/pkg/rgrpc/todo/pool"
+ "github.com/cs3org/reva/pkg/rhttp/global"
+ "github.com/cs3org/reva/pkg/sharedconf"
+ "github.com/mitchellh/mapstructure"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+)
+
+func init() {
+ global.Register("appprovider", New)
+}
+
+// Config holds the config options that need to be passed down to all ocdav handlers
+type Config struct {
+ Prefix string `mapstructure:"prefix"`
+ GatewaySvc string `mapstructure:"gatewaysvc"`
+ AccessTokenTTL int `mapstructure:"access_token_ttl"`
+}
+
+func (c *Config) init() {
+ if c.Prefix == "" {
+ c.Prefix = "api/v0/wopi/open"
+ }
+ if c.AccessTokenTTL == 0 {
+ c.AccessTokenTTL = 86400
+ }
+ c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
+}
+
+type svc struct {
+ conf *Config
+}
+
+// New returns a new ocmd object
+func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
+
+ conf := &Config{}
+ if err := mapstructure.Decode(m, conf); err != nil {
+ return nil, err
+ }
+ conf.init()
+
+ s := &svc{
+ conf: conf,
+ }
+ return s, nil
+}
+
+// Close performs cleanup.
+func (s *svc) Close() error {
+ return nil
+}
+
+func (s *svc) Prefix() string {
+ return s.conf.Prefix
+}
+
+func (s *svc) Unprotected() []string {
+ return []string{}
+}
+
+func (s *svc) Handler() http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ ocmd.WriteError(w, r, ocmd.APIErrorUnimplemented, "only GET requests are supported", errors.New("only GET requests are supported"))
+ return
+ }
+
+ s.handleWopiOpen(w, r)
+ })
+}
+
+// WopiResponse holds the various fields to be returned for a wopi open call
+type WopiResponse struct {
+ WopiClientURL string `json:"wopiclienturl"`
+ AccessToken string `json:"accesstoken"`
+ AccessTokenTTL int64 `json:"accesstokenttl"`
+}
+
+func (s *svc) handleWopiOpen(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
+ if err != nil {
+ ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error getting grpc gateway client", err)
+ return
+ }
+
+ info, errCode, err := s.getStatInfo(ctx, r.URL.Query().Get("fileId"), client)
+ if err != nil {
+ ocmd.WriteError(w, r, errCode, "error statting file", err)
+ }
+
+ openReq := gateway.OpenInAppRequest{
+ Ref: &provider.Reference{ResourceId: info.Id},
+ ViewMode: getViewMode(info),
+ App: r.URL.Query().Get("app"),
+ }
+ openRes, err := client.OpenInApp(ctx, &openReq)
+ if err != nil {
+ ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error opening resource", err)
+ return
+ }
+ if openRes.Status.Code != rpc.Code_CODE_OK {
+ ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error opening resource information", status.NewErrorFromCode(openRes.Status.Code, "appprovider"))
+ return
+ }
+
+ u, err := url.Parse(openRes.AppUrl)
+ if err != nil {
+ ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error parsing app URL", err)
+ return
+ }
+ q := u.Query()
+
+ // remove access token from query parameters
+ accessToken := q.Get("access_token")
+ q.Del("access_token")
+
+ // more options used by oC 10:
+ // &lang=en-GB
+ // &closebutton=1
+ // &revisionhistory=1
+ // &title=Hello.odt
+ u.RawQuery = q.Encode()
+
+ js, err := json.Marshal(
+ WopiResponse{
+ WopiClientURL: u.String(),
+ AccessToken: accessToken,
+ // https://wopi.readthedocs.io/projects/wopirest/en/latest/concepts.html#term-access-token-ttl
+ AccessTokenTTL: time.Now().Add(time.Second*time.Duration(s.conf.AccessTokenTTL)).UnixNano() / 1e6,
+ },
+ )
+ if err != nil {
+ ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error marshalling JSON response", err)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ if _, err = w.Write(js); err != nil {
+ ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error writing JSON response", err)
+ return
+ }
+}
+
+func (s *svc) getStatInfo(ctx context.Context, fileID string, client gateway.GatewayAPIClient) (*provider.ResourceInfo, ocmd.APIErrorCode, error) {
+ if fileID == "" {
+ return nil, ocmd.APIErrorInvalidParameter, errors.New("fileID parameter missing in request")
+ }
+
+ decodedID, err := base64.URLEncoding.DecodeString(fileID)
+ if err != nil {
+ return nil, ocmd.APIErrorInvalidParameter, errors.Wrap(err, "fileID doesn't follow the required format")
+ }
+
+ parts := strings.Split(string(decodedID), ":")
+ if !utf8.ValidString(parts[0]) || !utf8.ValidString(parts[1]) {
+ return nil, ocmd.APIErrorInvalidParameter, errors.New("fileID contains illegal characters")
+ }
+ res := &provider.ResourceId{
+ StorageId: parts[0],
+ OpaqueId: parts[1],
+ }
+
+ statReq := provider.StatRequest{
+ Ref: &provider.Reference{ResourceId: res},
+ }
+ statRes, err := client.Stat(ctx, &statReq)
+ if err != nil {
+ return nil, ocmd.APIErrorServerError, err
+ }
+ if statRes.Status.Code != rpc.Code_CODE_OK {
+ return nil, ocmd.APIErrorServerError, status.NewErrorFromCode(statRes.Status.Code, "appprovider")
+ }
+ if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
+ return nil, ocmd.APIErrorServerError, errors.New("unsupported resource type")
+ }
+
+ return statRes.Info, ocmd.APIErrorCode(""), nil
+}
+
+func getViewMode(res *provider.ResourceInfo) gateway.OpenInAppRequest_ViewMode {
+ var viewMode gateway.OpenInAppRequest_ViewMode
+ canEdit := res.PermissionSet.InitiateFileUpload
+ canView := res.PermissionSet.InitiateFileDownload
+
+ switch {
+ case canEdit && canView:
+ viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE
+ case canView:
+ viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_ONLY
+ default:
+ viewMode = gateway.OpenInAppRequest_VIEW_MODE_INVALID
+ }
+ return viewMode
+}
diff --git a/internal/http/services/ocmd/ocmd.go b/internal/http/services/ocmd/ocmd.go
index 61def86fc9..da6ebe46ba 100644
--- a/internal/http/services/ocmd/ocmd.go
+++ b/internal/http/services/ocmd/ocmd.go
@@ -30,6 +30,10 @@ import (
"github.com/rs/zerolog"
)
+func init() {
+ global.Register("ocmd", New)
+}
+
// Config holds the config options that need to be passed down to all ocdav handlers
type Config struct {
SMTPCredentials *smtpclient.SMTPCredentials `mapstructure:"smtp_credentials"`
@@ -56,10 +60,6 @@ type svc struct {
InvitesHandler *invitesHandler
}
-func init() {
- global.Register("ocmd", New)
-}
-
// New returns a new ocmd object
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
diff --git a/pkg/app/app.go b/pkg/app/app.go
index 1534094f2d..b4ff68925c 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -21,24 +21,24 @@ 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 {
- FindProvider(ctx context.Context, mimeType string) (*ProviderInfo, error)
- ListProviders(ctx context.Context) ([]*ProviderInfo, error)
-}
-
-// ProviderInfo contains the information
-// about a Application Provider
-type ProviderInfo struct {
- Location string
+ FindProviders(ctx context.Context, mimeType string) ([]*registry.ProviderInfo, error)
+ ListProviders(ctx context.Context) ([]*registry.ProviderInfo, 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 providing the iframe location to a iframe UI Provider
+// for providing the URL of the app which will serve the requested resource.
type Provider interface {
- GetIFrame(ctx context.Context, resID *provider.Reference, token string) (string, error)
+ GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token string) (string, error)
+ GetAppProviderInfo(ctx context.Context) (*registry.ProviderInfo, error)
}
diff --git a/pkg/app/provider/demo/demo.go b/pkg/app/provider/demo/demo.go
index 28134083e5..4805f2b1b9 100644
--- a/pkg/app/provider/demo/demo.go
+++ b/pkg/app/provider/demo/demo.go
@@ -24,19 +24,32 @@ import (
"github.com/cs3org/reva/pkg/app"
- providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+ 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/pkg/app/provider/registry"
"github.com/mitchellh/mapstructure"
)
-type provider struct {
+func init() {
+ registry.Register("demo", New)
+}
+
+type demoProvider struct {
iframeUIProvider string
}
-func (p *provider) GetIFrame(ctx context.Context, resID *providerpb.Reference, token string) (string, error) {
- msg := fmt.Sprintf("", p.iframeUIProvider, resID.ResourceId.StorageId+":"+resID.ResourceId.OpaqueId, token)
+func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token string) (string, error) {
+ msg := fmt.Sprintf("", p.iframeUIProvider, resource.Id.StorageId+":"+resource.Id.OpaqueId, viewMode.String(), token)
return msg, 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"`
}
@@ -49,12 +62,12 @@ func parseConfig(m map[string]interface{}) (*config, error) {
return c, nil
}
-// New returns an implementation to of the storage.FS interface that talk to
-// a local filesystem.
+// 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 &provider{iframeUIProvider: c.IFrameUIProvider}, nil
+ return &demoProvider{iframeUIProvider: c.IFrameUIProvider}, nil
}
diff --git a/pkg/app/provider/loader/loader.go b/pkg/app/provider/loader/loader.go
new file mode 100644
index 0000000000..f373136de9
--- /dev/null
+++ b/pkg/app/provider/loader/loader.go
@@ -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/pkg/app/provider/demo"
+ _ "github.com/cs3org/reva/pkg/app/provider/wopi"
+ // Add your own here
+)
diff --git a/pkg/app/provider/registry/registry.go b/pkg/app/provider/registry/registry.go
new file mode 100644
index 0000000000..43d5ed0188
--- /dev/null
+++ b/pkg/app/provider/registry/registry.go
@@ -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/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
+}
diff --git a/pkg/app/provider/wopi/wopi.go b/pkg/app/provider/wopi/wopi.go
new file mode 100644
index 0000000000..2562c9902a
--- /dev/null
+++ b/pkg/app/provider/wopi/wopi.go
@@ -0,0 +1,309 @@
+// 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 (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "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"
+ provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+ "github.com/cs3org/reva/pkg/app"
+ "github.com/cs3org/reva/pkg/app/provider/registry"
+ "github.com/cs3org/reva/pkg/appctx"
+ "github.com/cs3org/reva/pkg/mime"
+ "github.com/cs3org/reva/pkg/rhttp"
+ "github.com/cs3org/reva/pkg/user"
+ "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."`
+ AppName string `mapstructure:"app_name" docs:";The App user-friendly name."`
+ 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."`
+ 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")
+ }
+
+ 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 string) (string, error) {
+ log := appctx.GetLogger(ctx)
+
+ ext := path.Ext(resource.Path)
+ wopiurl, err := url.Parse(p.conf.WopiURL)
+ if err != nil {
+ return "", err
+ }
+ wopiurl.Path = path.Join(wopiurl.Path, "/wopi/iop/openinapp")
+
+ httpReq, err := rhttp.NewRequest(ctx, "GET", wopiurl.String(), nil)
+ if err != nil {
+ return "", err
+ }
+
+ q := httpReq.URL.Query()
+ q.Add("fileid", resource.GetId().OpaqueId)
+ q.Add("endpoint", resource.GetId().StorageId)
+ q.Add("viewmode", viewMode.String())
+ // TODO the folder URL should be resolved as e.g. `'https://cernbox.cern.ch/index.php/apps/files/?dir=' + filepath.Dir(req.Ref.GetPath())`
+ // or should be deprecated/removed altogether, needs discussion and decision.
+ // q.Add("folderurl", "...")
+ u, ok := user.ContextGetUser(ctx)
+ if ok { // else defaults to "Anonymous Guest"
+ q.Add("username", u.Username)
+ }
+
+ q.Add("appname", p.conf.AppName)
+ q.Add("appurl", p.appURLs["edit"][ext])
+
+ if p.conf.AppIntURL != "" {
+ q.Add("appinturl", p.conf.AppIntURL)
+ }
+ if viewExts, ok := p.appURLs["view"]; ok {
+ if url, ok := viewExts[ext]; ok {
+ q.Add("appviewurl", url)
+ }
+ }
+ 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)
+
+ openRes, err := p.wopiClient.Do(httpReq)
+ if err != nil {
+ return "", errors.Wrap(err, "wopi: error performing open request to WOPI server")
+ }
+ defer openRes.Body.Close()
+
+ if openRes.StatusCode != http.StatusFound {
+ return "", errors.Wrap(err, "wopi: unexpected status from WOPI server: "+openRes.Status)
+ }
+ appFullURL := openRes.Header.Get("Location")
+
+ log.Info().Msg(fmt.Sprintf("wopi: returning app URL %s", appFullURL))
+ return appFullURL, 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,
+ 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) {
+ // || (c.AppName != "CodiMD" && c.AppName != "Etherpad") {
+ 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
+ if c.AppName == "CodiMD" {
+ appURLs = getCodimdExtensions(c.AppURL)
+ } else if c.AppName == "Etherpad" {
+ appURLs = getEtherpadExtensions(c.AppURL)
+ }
+
+ }
+ return appURLs, nil
+}
+
+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")
+
+ 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" {
+ ext := action.SelectAttrValue("ext", "")
+ url := action.SelectAttrValue("urlsrc", "")
+
+ if ext == "" || url == "" {
+ continue
+ }
+
+ if _, ok := appURLs[access]; !ok {
+ appURLs[access] = make(map[string]string)
+ }
+ appURLs[access]["."+ext] = url
+ }
+ }
+ }
+ }
+ }
+ return appURLs, nil
+}
+
+func getCodimdExtensions(appURL string) map[string]map[string]string {
+ 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{
+ ".etherpad": appURL,
+ }
+ return appURLs
+}
diff --git a/pkg/app/registry/loader/loader.go b/pkg/app/registry/loader/loader.go
new file mode 100644
index 0000000000..7c06a89f92
--- /dev/null
+++ b/pkg/app/registry/loader/loader.go
@@ -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/pkg/app/registry/static"
+ // Add your own here
+)
diff --git a/pkg/app/registry/registry/registry.go b/pkg/app/registry/registry/registry.go
new file mode 100644
index 0000000000..0650a2a75f
--- /dev/null
+++ b/pkg/app/registry/registry/registry.go
@@ -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/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
+}
diff --git a/pkg/app/registry/static/static.go b/pkg/app/registry/static/static.go
index 6b419098d8..d883be7032 100644
--- a/pkg/app/registry/static/static.go
+++ b/pkg/app/registry/static/static.go
@@ -22,39 +22,84 @@ import (
"context"
"strings"
+ registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/cs3org/reva/pkg/app"
+ "github.com/cs3org/reva/pkg/app/registry/registry"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/sharedconf"
"github.com/mitchellh/mapstructure"
)
-type registry struct {
- rules map[string]string
+func init() {
+ registry.Register("static", New)
+}
+
+type config struct {
+ Providers map[string]*registrypb.ProviderInfo `mapstructure:"providers"`
}
func (c *config) init() {
- if len(c.Rules) == 0 {
- c.Rules = map[string]string{
- "text/plain": sharedconf.GetGatewaySVC(""),
+ if len(c.Providers) == 0 {
+ c.Providers = map[string]*registrypb.ProviderInfo{
+ sharedconf.GetGatewaySVC(""): ®istrypb.ProviderInfo{
+ Address: sharedconf.GetGatewaySVC(""),
+ MimeTypes: []string{"text/plain"},
+ },
}
}
}
-func (b *registry) ListProviders(ctx context.Context) ([]*app.ProviderInfo, error) {
- var providers = make([]*app.ProviderInfo, 0, len(b.rules))
- for _, address := range b.rules {
- providers = append(providers, &app.ProviderInfo{
- Location: address,
- })
+func parseConfig(m map[string]interface{}) (*config, error) {
+ c := &config{}
+ if err := mapstructure.Decode(m, c); err != nil {
+ return nil, err
}
- return providers, nil
+ return c, nil
+}
+
+type mimeTypeIndex struct {
+ defaultApp string
+ apps []string
+}
+
+type reg struct {
+ providers map[string]*registrypb.ProviderInfo
+ mimetypes map[string]*mimeTypeIndex // map the mime type to the addresses of the corresponding providers
+}
+
+// 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()
+
+ newReg := reg{
+ providers: c.Providers,
+ mimetypes: make(map[string]*mimeTypeIndex),
+ }
+
+ for addr, p := range c.Providers {
+ if p != nil {
+ for _, m := range p.MimeTypes {
+ _, ok := newReg.mimetypes[m]
+ if ok {
+ newReg.mimetypes[m].apps = append(newReg.mimetypes[m].apps, addr)
+ } else {
+ newReg.mimetypes[m] = &mimeTypeIndex{apps: []string{addr}}
+ }
+ }
+ }
+ }
+ return &newReg, nil
}
-func (b *registry) FindProvider(ctx context.Context, mimeType string) (*app.ProviderInfo, error) {
+func (b *reg) FindProviders(ctx context.Context, mimeType string) ([]*registrypb.ProviderInfo, error) {
// find longest match
var match string
- for prefix := range b.rules {
+ for prefix := range b.mimetypes {
if strings.HasPrefix(mimeType, prefix) && len(prefix) > len(match) {
match = prefix
}
@@ -64,31 +109,63 @@ func (b *registry) FindProvider(ctx context.Context, mimeType string) (*app.Prov
return nil, errtypes.NotFound("application provider not found for mime type " + mimeType)
}
- p := &app.ProviderInfo{
- Location: b.rules[match],
+ var providers = make([]*registrypb.ProviderInfo, 0, len(b.mimetypes[match].apps))
+ for _, p := range b.mimetypes[match].apps {
+ providers = append(providers, b.providers[p])
}
- return p, nil
+ return providers, nil
}
-type config struct {
- Rules map[string]string
+func (b *reg) AddProvider(ctx context.Context, p *registrypb.ProviderInfo) error {
+ b.providers[p.Address] = p
+
+ for _, m := range p.MimeTypes {
+ _, ok := b.mimetypes[m]
+ if ok {
+ b.mimetypes[m].apps = append(b.mimetypes[m].apps, p.Address)
+ } else {
+ b.mimetypes[m] = &mimeTypeIndex{apps: []string{p.Address}}
+ }
+ }
+ return nil
}
-func parseConfig(m map[string]interface{}) (*config, error) {
- c := &config{}
- if err := mapstructure.Decode(m, c); err != nil {
- return nil, err
+func (b *reg) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) {
+ var providers = make([]*registrypb.ProviderInfo, 0, len(b.providers))
+ for _, p := range b.providers {
+ providers = append(providers, p)
}
- return c, nil
+ return providers, nil
}
-// New returns an implementation to of the storage.FS interface that talk to
-// a local filesystem.
-func New(m map[string]interface{}) (app.Registry, error) {
- c, err := parseConfig(m)
- if err != nil {
- return nil, err
+func (b *reg) SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registrypb.ProviderInfo) error {
+ _, ok := b.mimetypes[mimeType]
+ if ok {
+ b.mimetypes[mimeType].defaultApp = p.Address
+ // Add to list of apps if not present
+ var present bool
+ for _, pr := range b.mimetypes[mimeType].apps {
+ if pr == p.Address {
+ present = true
+ break
+ }
+ }
+ if !present {
+ b.mimetypes[mimeType].apps = append(b.mimetypes[mimeType].apps, p.Address)
+ }
+ } else {
+ b.mimetypes[mimeType] = &mimeTypeIndex{apps: []string{p.Address}, defaultApp: p.Address}
}
- c.init()
- return ®istry{rules: c.Rules}, nil
+ return nil
+}
+
+func (b *reg) GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registrypb.ProviderInfo, error) {
+ _, ok := b.mimetypes[mimeType]
+ if ok {
+ p, ok := b.providers[b.mimetypes[mimeType].defaultApp]
+ if ok {
+ return p, nil
+ }
+ }
+ return nil, errtypes.NotFound("default application provider not set for mime type " + mimeType)
}
diff --git a/pkg/auth/manager/registry/registry.go b/pkg/auth/manager/registry/registry.go
index 37a164ae1b..52141f3d07 100644
--- a/pkg/auth/manager/registry/registry.go
+++ b/pkg/auth/manager/registry/registry.go
@@ -21,7 +21,7 @@ package registry
import "github.com/cs3org/reva/pkg/auth"
// NewFunc is the function that auth implementations
-// should register at init time.
+// should register to at init time.
type NewFunc func(map[string]interface{}) (auth.Manager, error)
// NewFuncs is a map containing all the registered auth managers.
diff --git a/pkg/auth/registry/registry/registry.go b/pkg/auth/registry/registry/registry.go
index 48e13898fc..0170885f37 100644
--- a/pkg/auth/registry/registry/registry.go
+++ b/pkg/auth/registry/registry/registry.go
@@ -20,14 +20,14 @@ package registry
import "github.com/cs3org/reva/pkg/auth"
-// NewFunc is the function that auth provider implementations
+// NewFunc is the function that auth registry implementations
// should register at init time.
type NewFunc func(map[string]interface{}) (auth.Registry, error)
-// NewFuncs is a map containing all the registered auth backends.
+// NewFuncs is a map containing all the registered auth registries.
var NewFuncs = map[string]NewFunc{}
-// Register registers a new auth provider new function.
+// Register registers a new auth registry new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f