rename folder extensions -> services
Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/webdav/pkg/metrics"
|
||||
)
|
||||
|
||||
// NewInstrument returns a service that instruments metrics.
|
||||
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
|
||||
return instrument{
|
||||
next: next,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
type instrument struct {
|
||||
next Service
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Thumbnail implements the Service interface.
|
||||
func (i instrument) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.Thumbnail(w, r)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// NewLogging returns a service that logs messages.
|
||||
func NewLogging(next Service, logger log.Logger) Service {
|
||||
return logging{
|
||||
next: next,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type logging struct {
|
||||
next Service
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Dummy implements the Service interface.
|
||||
func (l logging) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.Thumbnail(w, r)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware provides a function to set the middleware option.
|
||||
func Middleware(val ...func(http.Handler) http.Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.Middleware = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/owncloud/ocis/v2/extensions/webdav/pkg/net"
|
||||
"github.com/owncloud/ocis/v2/extensions/webdav/pkg/prop"
|
||||
"github.com/owncloud/ocis/v2/extensions/webdav/pkg/propfind"
|
||||
searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
merrors "go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
elementNameSearchFiles = "search-files"
|
||||
// TODO elementNameFilterFiles = "filter-files"
|
||||
)
|
||||
|
||||
// Search is the endpoint for retrieving search results for REPORT requests
|
||||
func (g Webdav) Search(w http.ResponseWriter, r *http.Request) {
|
||||
rep, err := readReport(r.Body)
|
||||
if err != nil {
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
g.log.Error().Err(err).Msg("error reading report")
|
||||
return
|
||||
}
|
||||
|
||||
if rep.SearchFiles == nil {
|
||||
renderError(w, r, errBadRequest("missing search-files tag"))
|
||||
g.log.Error().Err(err).Msg("error reading report")
|
||||
return
|
||||
}
|
||||
|
||||
t := r.Header.Get(TokenHeader)
|
||||
ctx := revactx.ContextSetToken(r.Context(), t)
|
||||
ctx = metadata.Set(ctx, revactx.TokenHeader, t)
|
||||
rsp, err := g.searchClient.Search(ctx, &searchsvc.SearchRequest{
|
||||
Query: rep.SearchFiles.Search.Pattern,
|
||||
})
|
||||
if err != nil {
|
||||
e := merrors.Parse(err.Error())
|
||||
switch e.Code {
|
||||
case http.StatusBadRequest:
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
default:
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
}
|
||||
g.log.Error().Err(err).Msg("could not get search results")
|
||||
return
|
||||
}
|
||||
|
||||
g.sendSearchResponse(rsp, w, r)
|
||||
}
|
||||
|
||||
func (g Webdav) sendSearchResponse(rsp *searchsvc.SearchResponse, w http.ResponseWriter, r *http.Request) {
|
||||
responsesXML, err := multistatusResponse(r.Context(), rsp.Matches)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("error formatting propfind")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
if _, err := w.Write(responsesXML); err != nil {
|
||||
g.log.Err(err).Msg("error writing response")
|
||||
}
|
||||
}
|
||||
|
||||
// multistatusResponse converts a list of matches into a multistatus response string
|
||||
func multistatusResponse(ctx context.Context, matches []*searchmsg.Match) ([]byte, error) {
|
||||
responses := make([]*propfind.ResponseXML, 0, len(matches))
|
||||
for i := range matches {
|
||||
res, err := matchToPropResponse(ctx, matches[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responses = append(responses, res)
|
||||
}
|
||||
|
||||
msr := propfind.NewMultiStatusResponseXML()
|
||||
msr.Responses = responses
|
||||
msg, err := xml.Marshal(msr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func matchToPropResponse(ctx context.Context, match *searchmsg.Match) (*propfind.ResponseXML, error) {
|
||||
response := propfind.ResponseXML{
|
||||
Href: net.EncodePath(path.Join("/dav/spaces/", match.Entity.Ref.ResourceId.StorageId+"!"+match.Entity.Ref.ResourceId.OpaqueId, match.Entity.Ref.Path)),
|
||||
Propstat: []propfind.PropstatXML{},
|
||||
}
|
||||
|
||||
propstatOK := propfind.PropstatXML{
|
||||
Status: "HTTP/1.1 200 OK",
|
||||
Prop: []prop.PropertyXML{},
|
||||
}
|
||||
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:fileid", match.Entity.Id.StorageId+"!"+match.Entity.Id.OpaqueId))
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getetag", match.Entity.Etag))
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getlastmodified", match.Entity.LastModifiedTime.AsTime().Format(time.RFC3339)))
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getcontenttype", match.Entity.MimeType))
|
||||
|
||||
size := strconv.FormatUint(match.Entity.Size, 10)
|
||||
if match.Entity.Type == uint64(provider.ResourceType_RESOURCE_TYPE_CONTAINER) {
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", "<d:collection/>"))
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:size", size))
|
||||
} else {
|
||||
propstatOK.Prop = append(propstatOK.Prop,
|
||||
prop.Escaped("d:resourcetype", ""),
|
||||
prop.Escaped("d:getcontentlength", size),
|
||||
)
|
||||
}
|
||||
score := strconv.FormatFloat(float64(match.Score), 'f', -1, 64)
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:score", score))
|
||||
|
||||
if len(propstatOK.Prop) > 0 {
|
||||
response.Propstat = append(response.Propstat, propstatOK)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
type report struct {
|
||||
SearchFiles *reportSearchFiles
|
||||
// FilterFiles TODO add this for tag based search
|
||||
FilterFiles *reportFilterFiles `xml:"filter-files"`
|
||||
}
|
||||
type reportSearchFiles struct {
|
||||
XMLName xml.Name `xml:"search-files"`
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
Prop Props `xml:"DAV: prop"`
|
||||
Search reportSearchFilesSearch `xml:"search"`
|
||||
}
|
||||
type reportSearchFilesSearch struct {
|
||||
Pattern string `xml:"pattern"`
|
||||
Limit int `xml:"limit"`
|
||||
Offset int `xml:"offset"`
|
||||
}
|
||||
|
||||
type reportFilterFiles struct {
|
||||
XMLName xml.Name `xml:"filter-files"`
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
Prop Props `xml:"DAV: prop"`
|
||||
Rules reportFilterFilesRules `xml:"filter-rules"`
|
||||
}
|
||||
|
||||
type reportFilterFilesRules struct {
|
||||
Favorite bool `xml:"favorite"`
|
||||
SystemTag int `xml:"systemtag"`
|
||||
}
|
||||
|
||||
// Props represents properties related to a resource
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind)
|
||||
type Props []xml.Name
|
||||
|
||||
// XML holds the xml representation of a propfind
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind
|
||||
type XML struct {
|
||||
XMLName xml.Name `xml:"DAV: propfind"`
|
||||
Allprop *struct{} `xml:"DAV: allprop"`
|
||||
Propname *struct{} `xml:"DAV: propname"`
|
||||
Prop Props `xml:"DAV: prop"`
|
||||
Include Props `xml:"DAV: include"`
|
||||
}
|
||||
|
||||
func readReport(r io.Reader) (rep *report, err error) {
|
||||
decoder := xml.NewDecoder(r)
|
||||
rep = &report{}
|
||||
for {
|
||||
t, err := decoder.Token()
|
||||
if err == io.EOF {
|
||||
// io.EOF is a successful end
|
||||
return rep, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := t.(xml.StartElement); ok {
|
||||
if v.Name.Local == elementNameSearchFiles {
|
||||
var repSF reportSearchFiles
|
||||
err = decoder.DecodeElement(&repSF, &v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rep.SearchFiles = &repSF
|
||||
/*
|
||||
} else if v.Name.Local == elementNameFilterFiles {
|
||||
var repFF reportFilterFiles
|
||||
err = decoder.DecodeElement(&repFF, &v)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
rep.FilterFiles = &repFF
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/storage/utils/templates"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
merrors "go-micro.dev/v4/errors"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/extensions/webdav/pkg/constants"
|
||||
"github.com/owncloud/ocis/v2/extensions/webdav/pkg/dav/requests"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
||||
thumbnailsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/thumbnails/v0"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
thumbnailssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/thumbnails/v0"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenHeader = "X-Access-Token"
|
||||
)
|
||||
|
||||
var (
|
||||
codesEnum = map[int]string{
|
||||
http.StatusBadRequest: "Sabre\\DAV\\Exception\\BadRequest",
|
||||
http.StatusUnauthorized: "Sabre\\DAV\\Exception\\NotAuthenticated",
|
||||
http.StatusNotFound: "Sabre\\DAV\\Exception\\NotFound",
|
||||
http.StatusMethodNotAllowed: "Sabre\\DAV\\Exception\\MethodNotAllowed",
|
||||
}
|
||||
)
|
||||
|
||||
// Service defines the extension handlers.
|
||||
type Service interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||
Thumbnail(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// NewService returns a service implementation for Service.
|
||||
func NewService(opts ...Option) (Service, error) {
|
||||
options := newOptions(opts...)
|
||||
conf := options.Config
|
||||
|
||||
m := chi.NewMux()
|
||||
// Comment back in after resolving the issue in go-chi.
|
||||
// See comment in line 82.
|
||||
// chi.RegisterMethod("REPORT")
|
||||
m.Use(options.Middleware...)
|
||||
|
||||
gwc, err := pool.GetGatewayServiceClient(conf.RevaGateway)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc := Webdav{
|
||||
config: conf,
|
||||
log: options.Logger,
|
||||
mux: m,
|
||||
searchClient: searchsvc.NewSearchProviderService("com.owncloud.api.search", grpc.DefaultClient),
|
||||
thumbnailsClient: thumbnailssvc.NewThumbnailService("com.owncloud.api.thumbnails", grpc.DefaultClient),
|
||||
revaClient: gwc,
|
||||
}
|
||||
|
||||
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(svc.DavUserContext())
|
||||
|
||||
r.Get("/remote.php/dav/spaces/{id}", svc.SpacesThumbnail)
|
||||
r.Get("/remote.php/dav/spaces/{id}/*", svc.SpacesThumbnail)
|
||||
r.Get("/dav/spaces/{id}", svc.SpacesThumbnail)
|
||||
r.Get("/dav/spaces/{id}/*", svc.SpacesThumbnail)
|
||||
|
||||
r.Get("/remote.php/dav/files/{id}", svc.Thumbnail)
|
||||
r.Get("/remote.php/dav/files/{id}/*", svc.Thumbnail)
|
||||
r.Get("/dav/files/{id}", svc.Thumbnail)
|
||||
r.Get("/dav/files/{id}/*", svc.Thumbnail)
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(svc.DavPublicContext())
|
||||
|
||||
r.Head("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnailHead)
|
||||
r.Head("/dav/public-files/{token}/*", svc.PublicThumbnailHead)
|
||||
|
||||
r.Get("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnail)
|
||||
r.Get("/dav/public-files/{token}/*", svc.PublicThumbnail)
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(svc.WebDAVContext())
|
||||
r.Get("/remote.php/webdav/*", svc.Thumbnail)
|
||||
r.Get("/webdav/*", svc.Thumbnail)
|
||||
})
|
||||
|
||||
// r.MethodFunc("REPORT", "/remote.php/dav/files/{id}/*", svc.Search)
|
||||
|
||||
// This is a workaround for the go-chi concurrent map read write issue.
|
||||
// After the issue has been solved upstream in go-chi we should switch
|
||||
// back to using `chi.RegisterMethod`.
|
||||
m.MethodNotAllowed(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
routePrefix := path.Join(options.Config.HTTP.Root, "/remote.php/dav/files/")
|
||||
if req.Method == "REPORT" && strings.HasPrefix(req.URL.Path, routePrefix) {
|
||||
// The URLParam will not be available here. If it is needed it
|
||||
// needs to be passed manually or chi needs to be fixed
|
||||
// To use it properly.
|
||||
svc.Search(w, req)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}))
|
||||
})
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// Webdav implements the business logic for Service.
|
||||
type Webdav struct {
|
||||
config *config.Config
|
||||
log log.Logger
|
||||
mux *chi.Mux
|
||||
searchClient searchsvc.SearchProviderService
|
||||
thumbnailsClient thumbnailssvc.ThumbnailService
|
||||
revaClient gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (g Webdav) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
g.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (g Webdav) DavUserContext() func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
filePath := r.URL.Path
|
||||
|
||||
id := chi.URLParam(r, "id")
|
||||
id, err := url.QueryUnescape(id)
|
||||
if err == nil && id != "" {
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyID, id)
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
filePath = strings.TrimPrefix(filePath, path.Join("/remote.php/dav/spaces", id))
|
||||
filePath = strings.TrimPrefix(filePath, path.Join("/dav/spaces", id))
|
||||
|
||||
filePath = strings.TrimPrefix(filePath, path.Join("/remote.php/dav/files", id))
|
||||
filePath = strings.TrimPrefix(filePath, path.Join("/dav/files", id))
|
||||
filePath = strings.TrimPrefix(filePath, "/")
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyPath, filePath)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (g Webdav) DavPublicContext() func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
filePath := r.URL.Path
|
||||
|
||||
if token := chi.URLParam(r, "token"); token != "" {
|
||||
filePath = strings.TrimPrefix(filePath, path.Join("/remote.php/dav/public-files", token)+"/")
|
||||
filePath = strings.TrimPrefix(filePath, path.Join("/dav/public-files", token)+"/")
|
||||
}
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyPath, filePath)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
func (g Webdav) WebDAVContext() func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
filePath := r.URL.Path
|
||||
filePath = strings.TrimPrefix(filePath, "/remote.php")
|
||||
filePath = strings.TrimPrefix(filePath, "/webdav/")
|
||||
|
||||
ctx := context.WithValue(r.Context(), constants.ContextKeyPath, filePath)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SpacesThumbnail is the endpoint for retrieving thumbnails inside of spaces.
|
||||
func (g Webdav) SpacesThumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
tr, err := requests.ParseThumbnailRequest(r)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("could not create Request")
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
t := r.Header.Get(TokenHeader)
|
||||
|
||||
fullPath := filepath.Join(tr.Identifier, tr.Filepath)
|
||||
rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
|
||||
Filepath: strings.TrimLeft(tr.Filepath, "/"),
|
||||
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
|
||||
Width: tr.Width,
|
||||
Height: tr.Height,
|
||||
Source: &thumbnailssvc.GetThumbnailRequest_Cs3Source{
|
||||
Cs3Source: &thumbnailsmsg.CS3Source{
|
||||
Path: fullPath,
|
||||
Authorization: t,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
e := merrors.Parse(err.Error())
|
||||
switch e.Code {
|
||||
case http.StatusNotFound:
|
||||
// StatusNotFound is expected for unsupported files
|
||||
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
|
||||
return
|
||||
case http.StatusBadRequest:
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
default:
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
}
|
||||
g.log.Error().Err(err).Msg("could not get thumbnail")
|
||||
return
|
||||
}
|
||||
|
||||
g.sendThumbnailResponse(rsp, w, r)
|
||||
}
|
||||
|
||||
// Thumbnail implements the Service interface.
|
||||
func (g Webdav) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
tr, err := requests.ParseThumbnailRequest(r)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("could not create Request")
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
t := r.Header.Get(TokenHeader)
|
||||
|
||||
var user *userv1beta1.User
|
||||
|
||||
if tr.Identifier == "" {
|
||||
// look up user from token via WhoAmI
|
||||
userRes, err := g.revaClient.WhoAmI(r.Context(), &gatewayv1beta1.WhoAmIRequest{
|
||||
Token: t,
|
||||
})
|
||||
if err != nil || userRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
g.log.Error().Err(err).Msg("could not get user")
|
||||
renderError(w, r, errInternalError("could not get user"))
|
||||
return
|
||||
}
|
||||
user = userRes.GetUser()
|
||||
} else {
|
||||
// look up user from URL via GetUserByClaim
|
||||
ctx := metadata.AppendToOutgoingContext(r.Context(), TokenHeader, t)
|
||||
userRes, err := g.revaClient.GetUserByClaim(ctx, &userv1beta1.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: tr.Identifier,
|
||||
})
|
||||
if err != nil || userRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
g.log.Error().Err(err).Msg("could not get user")
|
||||
renderError(w, r, errInternalError("could not get user"))
|
||||
return
|
||||
}
|
||||
user = userRes.GetUser()
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(templates.WithUser(user, g.config.WebdavNamespace), tr.Filepath)
|
||||
rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
|
||||
Filepath: strings.TrimLeft(tr.Filepath, "/"),
|
||||
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
|
||||
Width: tr.Width,
|
||||
Height: tr.Height,
|
||||
Source: &thumbnailssvc.GetThumbnailRequest_Cs3Source{
|
||||
Cs3Source: &thumbnailsmsg.CS3Source{
|
||||
Path: fullPath,
|
||||
Authorization: t,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
e := merrors.Parse(err.Error())
|
||||
switch e.Code {
|
||||
case http.StatusNotFound:
|
||||
// StatusNotFound is expected for unsupported files
|
||||
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
|
||||
return
|
||||
case http.StatusBadRequest:
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
default:
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
}
|
||||
g.log.Error().Err(err).Msg("could not get thumbnail")
|
||||
return
|
||||
}
|
||||
|
||||
g.sendThumbnailResponse(rsp, w, r)
|
||||
}
|
||||
|
||||
func (g Webdav) PublicThumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
tr, err := requests.ParseThumbnailRequest(r)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("could not create Request")
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
|
||||
Filepath: strings.TrimLeft(tr.Filepath, "/"),
|
||||
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
|
||||
Width: tr.Width,
|
||||
Height: tr.Height,
|
||||
Source: &thumbnailssvc.GetThumbnailRequest_WebdavSource{
|
||||
WebdavSource: &thumbnailsmsg.WebdavSource{
|
||||
Url: g.config.OcisPublicURL + r.URL.RequestURI(),
|
||||
IsPublicLink: true,
|
||||
PublicLinkToken: tr.PublicLinkToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
e := merrors.Parse(err.Error())
|
||||
switch e.Code {
|
||||
case http.StatusNotFound:
|
||||
// StatusNotFound is expected for unsupported files
|
||||
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
|
||||
return
|
||||
case http.StatusBadRequest:
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
default:
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
}
|
||||
g.log.Error().Err(err).Msg("could not get thumbnail")
|
||||
return
|
||||
}
|
||||
|
||||
g.sendThumbnailResponse(rsp, w, r)
|
||||
}
|
||||
|
||||
func (g Webdav) PublicThumbnailHead(w http.ResponseWriter, r *http.Request) {
|
||||
tr, err := requests.ParseThumbnailRequest(r)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("could not create Request")
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
|
||||
Filepath: strings.TrimLeft(tr.Filepath, "/"),
|
||||
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
|
||||
Width: tr.Width,
|
||||
Height: tr.Height,
|
||||
Source: &thumbnailssvc.GetThumbnailRequest_WebdavSource{
|
||||
WebdavSource: &thumbnailsmsg.WebdavSource{
|
||||
Url: g.config.OcisPublicURL + r.URL.RequestURI(),
|
||||
IsPublicLink: true,
|
||||
PublicLinkToken: tr.PublicLinkToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
e := merrors.Parse(err.Error())
|
||||
switch e.Code {
|
||||
case http.StatusNotFound:
|
||||
// StatusNotFound is expected for unsupported files
|
||||
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
|
||||
return
|
||||
case http.StatusBadRequest:
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
default:
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
}
|
||||
g.log.Error().Err(err).Msg("could not get thumbnail")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (g Webdav) sendThumbnailResponse(rsp *thumbnailssvc.GetThumbnailResponse, w http.ResponseWriter, r *http.Request) {
|
||||
client := &http.Client{
|
||||
// Timeout: time.Second * 5,
|
||||
}
|
||||
|
||||
dlReq, err := http.NewRequest(http.MethodGet, rsp.DataEndpoint, http.NoBody)
|
||||
if err != nil {
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
g.log.Error().Err(err).Msg("could not download thumbnail")
|
||||
return
|
||||
}
|
||||
dlReq.Header.Set("Transfer-Token", rsp.TransferToken)
|
||||
|
||||
dlRsp, err := client.Do(dlReq)
|
||||
if err != nil {
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
g.log.Error().Err(err).Msg("could not download thumbnail")
|
||||
return
|
||||
}
|
||||
defer dlRsp.Body.Close()
|
||||
|
||||
if dlRsp.StatusCode != http.StatusOK {
|
||||
g.log.Error().
|
||||
Str("transfer_token", rsp.TransferToken).
|
||||
Str("data_endpoint", rsp.DataEndpoint).
|
||||
Str("response_status", dlRsp.Status).
|
||||
Msg("could not download thumbnail")
|
||||
renderError(w, r, errInternalError("could not download thumbnail"))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", rsp.Mimetype)
|
||||
_, err = io.Copy(w, dlRsp.Body)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("failed to write thumbnail to response writer")
|
||||
}
|
||||
}
|
||||
|
||||
func extensionToThumbnailType(ext string) thumbnailsmsg.ThumbnailType {
|
||||
switch strings.ToUpper(ext) {
|
||||
case "GIF":
|
||||
return thumbnailsmsg.ThumbnailType_GIF
|
||||
case "PNG":
|
||||
return thumbnailsmsg.ThumbnailType_PNG
|
||||
default:
|
||||
return thumbnailsmsg.ThumbnailType_JPG
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
|
||||
type errResponse struct {
|
||||
HTTPStatusCode int `json:"-" xml:"-"`
|
||||
XMLName xml.Name `xml:"d:error"`
|
||||
Xmlnsd string `xml:"xmlns:d,attr"`
|
||||
Xmlnss string `xml:"xmlns:s,attr"`
|
||||
Exception string `xml:"s:exception"`
|
||||
Message string `xml:"s:message"`
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func newErrResponse(statusCode int, msg string) *errResponse {
|
||||
rsp := &errResponse{
|
||||
HTTPStatusCode: statusCode,
|
||||
Xmlnsd: "DAV",
|
||||
Xmlnss: "http://sabredav.org/ns",
|
||||
Exception: codesEnum[statusCode],
|
||||
}
|
||||
if msg != "" {
|
||||
rsp.Message = msg
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
|
||||
func errInternalError(msg string) *errResponse {
|
||||
return newErrResponse(http.StatusInternalServerError, msg)
|
||||
}
|
||||
|
||||
func errBadRequest(msg string) *errResponse {
|
||||
return newErrResponse(http.StatusBadRequest, msg)
|
||||
}
|
||||
|
||||
func errNotFound(msg string) *errResponse {
|
||||
return newErrResponse(http.StatusNotFound, msg)
|
||||
}
|
||||
|
||||
func renderError(w http.ResponseWriter, r *http.Request, err *errResponse) {
|
||||
render.Status(r, err.HTTPStatusCode)
|
||||
render.XML(w, r, err)
|
||||
}
|
||||
|
||||
func notFoundMsg(name string) string {
|
||||
return "File with name " + name + " could not be located"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NewTracing returns a service that instruments traces.
|
||||
func NewTracing(next Service) Service {
|
||||
return tracing{
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
type tracing struct {
|
||||
next Service
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
t.next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Thumbnail implements the Service interface.
|
||||
func (t tracing) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
t.next.Thumbnail(w, r)
|
||||
}
|
||||
Reference in New Issue
Block a user