12216: Add webdav handler.
authorTom Clegg <tclegg@veritasgenetics.com>
Wed, 11 Oct 2017 21:17:02 +0000 (17:17 -0400)
committerTom Clegg <tclegg@veritasgenetics.com>
Thu, 12 Oct 2017 04:03:00 +0000 (00:03 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

sdk/go/arvados/collection_fs.go
services/keep-web/handler.go
services/keep-web/handler_test.go
services/keep-web/webdav.go [new file with mode: 0644]
vendor/vendor.json

index f80180f8ed39d2c1c4a9c60500cea2f2967906e4..8ecbe9c197de49da13a6a8d4ded90d1cba83e198 100644 (file)
@@ -10,6 +10,7 @@ import (
        "os"
        "path"
        "strings"
+       "sync"
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/manifest"
@@ -150,6 +151,8 @@ type collectionFS struct {
        collection *Collection
        client     *Client
        kc         keepClient
+       sizes      map[string]int64
+       sizesOnce  sync.Once
 }
 
 // FileSystem returns an http.FileSystem for the collection.
@@ -225,15 +228,14 @@ func (c *collectionFS) Open(name string) (http.File, error) {
 // fileSizes returns a map of files that can be opened. Each key
 // starts with "./".
 func (c *collectionFS) fileSizes() map[string]int64 {
-       var sizes map[string]int64
-       m := manifest.Manifest{Text: c.collection.ManifestText}
-       for ms := range m.StreamIter() {
-               for _, fss := range ms.FileStreamSegments {
-                       if sizes == nil {
-                               sizes = map[string]int64{}
+       c.sizesOnce.Do(func() {
+               c.sizes = map[string]int64{}
+               m := manifest.Manifest{Text: c.collection.ManifestText}
+               for ms := range m.StreamIter() {
+                       for _, fss := range ms.FileStreamSegments {
+                               c.sizes[ms.StreamName+"/"+fss.Name] += int64(fss.SegLen)
                        }
-                       sizes[ms.StreamName+"/"+fss.Name] += int64(fss.SegLen)
                }
-       }
-       return sizes
+       })
+       return c.sizes
 }
index 67d46f6716b2ce22b09cb6b1204b08fdb3c3dd96..432fc21f4e9bb3f642ead4b3b51e17b919b016fc 100644 (file)
@@ -24,6 +24,7 @@ import (
        "git.curoverse.com/arvados.git/sdk/go/health"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
        "git.curoverse.com/arvados.git/sdk/go/keepclient"
+       "golang.org/x/net/webdav"
 )
 
 type handler struct {
@@ -31,6 +32,7 @@ type handler struct {
        clientPool    *arvadosclient.ClientPool
        setupOnce     sync.Once
        healthHandler http.Handler
+       webdavLS      webdav.LockSystem
 }
 
 // parseCollectionIDFromDNSName returns a UUID or PDH if s begins with
@@ -79,6 +81,8 @@ func (h *handler) setup() {
                Token:  h.Config.ManagementToken,
                Prefix: "/_health/",
        }
+
+       h.webdavLS = webdav.NewMemLS()
 }
 
 func (h *handler) serveStatus(w http.ResponseWriter, r *http.Request) {
@@ -90,6 +94,20 @@ func (h *handler) serveStatus(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(status)
 }
 
+var (
+       webdavMethod = map[string]bool{
+               "OPTIONS":  true,
+               "PROPFIND": true,
+               "LOCK":     true,
+               "UNLOCK":   true,
+       }
+       fsMethod = map[string]bool{
+               "GET":  true,
+               "HEAD": true,
+               "POST": true,
+       }
+)
+
 // ServeHTTP implements http.Handler.
 func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
        h.setupOnce.Do(h.setup)
@@ -123,21 +141,20 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
                return
        }
 
-       if r.Method == "OPTIONS" {
-               method := r.Header.Get("Access-Control-Request-Method")
-               if method != "GET" && method != "POST" {
+       if method := r.Header.Get("Access-Control-Request-Method"); method != "" && r.Method == "OPTIONS" {
+               if !fsMethod[method] && !webdavMethod[method] {
                        statusCode = http.StatusMethodNotAllowed
                        return
                }
                w.Header().Set("Access-Control-Allow-Headers", "Range")
-               w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
+               w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PROPFIND, LOCK, UNLOCK")
                w.Header().Set("Access-Control-Allow-Origin", "*")
                w.Header().Set("Access-Control-Max-Age", "86400")
                statusCode = http.StatusOK
                return
        }
 
-       if r.Method != "GET" && r.Method != "POST" {
+       if !fsMethod[r.Method] && !webdavMethod[r.Method] {
                statusCode, statusText = http.StatusMethodNotAllowed, r.Method
                return
        }
@@ -337,6 +354,23 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
                AuthToken: arv.ApiToken,
                Insecure:  arv.ApiInsecure,
        }, kc)
+       if webdavMethod[r.Method] {
+               h := webdav.Handler{
+                       Prefix:     "/" + strings.Join(pathParts[:stripParts], "/"),
+                       FileSystem: &webdavFS{httpfs: fs},
+                       LockSystem: h.webdavLS,
+                       Logger: func(_ *http.Request, err error) {
+                               if os.IsNotExist(err) {
+                                       statusCode, statusText = http.StatusNotFound, err.Error()
+                               } else if err != nil {
+                                       statusCode, statusText = http.StatusInternalServerError, err.Error()
+                               }
+                       },
+               }
+               h.ServeHTTP(w, r)
+               return
+       }
+
        openPath := "/" + strings.Join(targetPath, "/")
        if f, err := fs.Open(openPath); os.IsNotExist(err) {
                // Requested non-existent path
index 04859595e6f337b7975f9305e05189c5aa6f3ce2..d0e92ee079c24d6057d226477bfbae5098a0868f 100644 (file)
@@ -43,7 +43,7 @@ func (s *UnitSuite) TestCORSPreflight(c *check.C) {
        c.Check(resp.Code, check.Equals, http.StatusOK)
        c.Check(resp.Body.String(), check.Equals, "")
        c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
-       c.Check(resp.Header().Get("Access-Control-Allow-Methods"), check.Equals, "GET, POST")
+       c.Check(resp.Header().Get("Access-Control-Allow-Methods"), check.Equals, "GET, POST, OPTIONS, PROPFIND, LOCK, UNLOCK")
        c.Check(resp.Header().Get("Access-Control-Allow-Headers"), check.Equals, "Range")
 
        // Check preflight for a disallowed request
diff --git a/services/keep-web/webdav.go b/services/keep-web/webdav.go
new file mode 100644 (file)
index 0000000..1b5811d
--- /dev/null
@@ -0,0 +1,62 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+       "errors"
+       "net/http"
+       "os"
+
+       "golang.org/x/net/context"
+       "golang.org/x/net/webdav"
+)
+
+var errReadOnly = errors.New("read-only filesystem")
+
+// webdavFS implements a read-only webdav.FileSystem by wrapping
+// http.Filesystem.
+type webdavFS struct {
+       httpfs http.FileSystem
+}
+
+var _ webdav.FileSystem = &webdavFS{}
+
+func (fs *webdavFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
+       return errReadOnly
+}
+
+func (fs *webdavFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
+       f, err := fs.httpfs.Open(name)
+       if err != nil {
+               return nil, err
+       }
+       return &webdavFile{File: f}, nil
+}
+
+func (fs *webdavFS) RemoveAll(ctx context.Context, name string) error {
+       return errReadOnly
+}
+
+func (fs *webdavFS) Rename(ctx context.Context, oldName, newName string) error {
+       return errReadOnly
+}
+
+func (fs *webdavFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
+       if f, err := fs.httpfs.Open(name); err != nil {
+               return nil, err
+       } else {
+               return f.Stat()
+       }
+}
+
+// webdavFile implements a read-only webdav.File by wrapping
+// http.File. Writes fail.
+type webdavFile struct {
+       http.File
+}
+
+func (f *webdavFile) Write([]byte) (int, error) {
+       return 0, errReadOnly
+}
index f9c46e7325aed6358e1cc8296d0a364fea27a908..8551a06fb414573e90d2a802c2806c99e8bf0bd5 100644 (file)
                        "revision": "280327cb4d1e1fe4f118d00596ce0b3a6ae6d07e",
                        "revisionTime": "2017-05-17T20:48:28Z"
                },
+               {
+                       "checksumSHA1": "yppNZB5y0GmJrt/TYOASrhe2oVc=",
+                       "path": "golang.org/x/net/webdav",
+                       "revision": "f01ecb60fe3835d80d9a0b7b2bf24b228c89260e",
+                       "revisionTime": "2017-07-11T11:58:19Z"
+               },
+               {
+                       "checksumSHA1": "XgtZlzd39qIkBHs6XYrq9dhTCog=",
+                       "path": "golang.org/x/net/webdav/internal/xml",
+                       "revision": "f01ecb60fe3835d80d9a0b7b2bf24b228c89260e",
+                       "revisionTime": "2017-07-11T11:58:19Z"
+               },
                {
                        "checksumSHA1": "7EZyXN0EmZLgGxZxK01IJua4c8o=",
                        "path": "golang.org/x/net/websocket",