From 68d0b5adbbf7d7deccc751b313d8020f8e42a107 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Tue, 25 Apr 2017 14:57:59 +0100 Subject: [PATCH] serve webdav: this implements a webdav server for any rclone remote. --- cmd/serve/serve.go | 2 + cmd/serve/webdav/webdav.go | 140 ++++++++++++++++++++++++++++++++ cmd/serve/webdav/webdav_test.go | 63 ++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 cmd/serve/webdav/webdav.go create mode 100644 cmd/serve/webdav/webdav_test.go diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go index ad99df14d..5314c4a8a 100644 --- a/cmd/serve/serve.go +++ b/cmd/serve/serve.go @@ -5,11 +5,13 @@ import ( "github.com/ncw/rclone/cmd" "github.com/ncw/rclone/cmd/serve/http" + "github.com/ncw/rclone/cmd/serve/webdav" "github.com/spf13/cobra" ) func init() { Command.AddCommand(http.Command) + Command.AddCommand(webdav.Command) cmd.Root.AddCommand(Command) } diff --git a/cmd/serve/webdav/webdav.go b/cmd/serve/webdav/webdav.go new file mode 100644 index 000000000..c52ad7fcf --- /dev/null +++ b/cmd/serve/webdav/webdav.go @@ -0,0 +1,140 @@ +package webdav + +// FIXME need to fix directory listings reading each file - make an +// override for getcontenttype property? + +import ( + "net/http" + "os" + + "github.com/ncw/rclone/cmd" + "github.com/ncw/rclone/fs" + "github.com/ncw/rclone/vfs" + "github.com/ncw/rclone/vfs/vfsflags" + "github.com/spf13/cobra" + "golang.org/x/net/context" + "golang.org/x/net/webdav" +) + +// Globals +var ( + bindAddress = "localhost:8081" +) + +func init() { + Command.Flags().StringVarP(&bindAddress, "addr", "", bindAddress, "IPaddress:Port to bind server to.") + vfsflags.AddFlags(Command.Flags()) +} + +// Command definition for cobra +var Command = &cobra.Command{ + Use: "webdav remote:path", + Short: `Serve remote:path over webdav.`, + Long: ` +rclone serve webdav implements a basic webdav server to serve the +remote over HTTP via the webdav protocol. This can be viewed with a +webdav client or you can make a remote of type webdav to read and +write it. + +FIXME at the moment each directory listing reads the start of each +file which is undesirable +`, + Run: func(command *cobra.Command, args []string) { + cmd.CheckArgs(1, 1, command, args) + fsrc := cmd.NewFsSrc(args) + cmd.Run(false, false, command, func() error { + return serveWebDav(fsrc) + }) + }, +} + +// serve the remote +func serveWebDav(f fs.Fs) error { + fs.Logf(f, "WebDav Server started on %v", bindAddress) + + webdavFS := &WebDAV{ + f: f, + vfs: vfs.New(f, &vfsflags.Opt), + } + + handler := &webdav.Handler{ + FileSystem: webdavFS, + LockSystem: webdav.NewMemLS(), + Logger: webdavFS.logRequest, // FIXME + } + + // FIXME use our HTTP transport + http.Handle("/", handler) + return http.ListenAndServe(bindAddress, nil) +} + +// WebDAV is a webdav.FileSystem interface +// +// A FileSystem implements access to a collection of named files. The elements +// in a file path are separated by slash ('/', U+002F) characters, regardless +// of host operating system convention. +// +// Each method has the same semantics as the os package's function of the same +// name. +// +// Note that the os.Rename documentation says that "OS-specific restrictions +// might apply". In particular, whether or not renaming a file or directory +// overwriting another existing file or directory is an error is OS-dependent. +type WebDAV struct { + f fs.Fs + vfs *vfs.VFS +} + +// check interface +var _ webdav.FileSystem = (*WebDAV)(nil) + +// logRequest is called by the webdav module on every request +func (w *WebDAV) logRequest(r *http.Request, err error) { + fs.Infof(r.URL.Path, "%s from %s", r.Method, r.RemoteAddr) +} + +// Mkdir creates a directory +func (w *WebDAV) Mkdir(ctx context.Context, name string, perm os.FileMode) (err error) { + defer fs.Trace(name, "perm=%v", perm)("err = %v", &err) + dir, leaf, err := w.vfs.StatParent(name) + if err != nil { + return err + } + _, err = dir.Mkdir(leaf) + return err +} + +// OpenFile opens a file or a directory +func (w *WebDAV) OpenFile(ctx context.Context, name string, flags int, perm os.FileMode) (file webdav.File, err error) { + defer fs.Trace(name, "flags=%v, perm=%v", flags, perm)("err = %v", &err) + return w.vfs.OpenFile(name, flags, perm) +} + +// RemoveAll removes a file or a directory and its contents +func (w *WebDAV) RemoveAll(ctx context.Context, name string) (err error) { + defer fs.Trace(name, "")("err = %v", &err) + node, err := w.vfs.Stat(name) + if err != nil { + return err + } + err = node.RemoveAll() + if err != nil { + return err + } + return nil +} + +// Rename a file or a directory +func (w *WebDAV) Rename(ctx context.Context, oldName, newName string) (err error) { + defer fs.Trace(oldName, "newName=%q", newName)("err = %v", &err) + return w.vfs.Rename(oldName, newName) +} + +// Stat returns info about the file or directory +func (w *WebDAV) Stat(ctx context.Context, name string) (fi os.FileInfo, err error) { + defer fs.Trace(name, "")("fi=%+v, err = %v", &fi, &err) + return w.vfs.Stat(name) +} + +// check interface +var _ os.FileInfo = vfs.Node(nil) diff --git a/cmd/serve/webdav/webdav_test.go b/cmd/serve/webdav/webdav_test.go new file mode 100644 index 000000000..496239bde --- /dev/null +++ b/cmd/serve/webdav/webdav_test.go @@ -0,0 +1,63 @@ +// Serve webdav tests set up a server and run the integration tests +// for the webdav remote against it. +// +// We skip tests on platforms with troublesome character mappings + +//+build !windows,!darwin + +package webdav + +import ( + "os" + "os/exec" + "testing" + + "github.com/ncw/rclone/fstest" + _ "github.com/ncw/rclone/local" + "github.com/stretchr/testify/assert" +) + +// TestWebDav runs the webdav server then runs the unit tests for the +// webdav remote against it. +func TestWebDav(t *testing.T) { + fstest.Initialise() + + fremote, _, clean, err := fstest.RandomRemote(*fstest.RemoteName, *fstest.SubDir) + assert.NoError(t, err) + defer clean() + + err = fremote.Mkdir("") + assert.NoError(t, err) + + // Start the server + go func() { + err := serveWebDav(fremote) + assert.NoError(t, err) + }() + // FIXME shut it down somehow? + + // Change directory to run the tests + err = os.Chdir("../../../webdav") + assert.NoError(t, err, "failed to cd to webdav remote") + + // Run the webdav tests with an on the fly remote + args := []string{"test"} + if testing.Verbose() { + args = append(args, "-v") + } + if *fstest.Verbose { + args = append(args, "-verbose") + } + args = append(args, "-remote", "webdavtest:") + cmd := exec.Command("go", args...) + cmd.Env = append(os.Environ(), + "RCLONE_CONFIG_WEBDAVTEST_TYPE=webdav", + "RCLONE_CONFIG_WEBDAVTEST_URL=http://localhost:8081/", + "RCLONE_CONFIG_WEBDAVTEST_VENDOR=other", + ) + out, err := cmd.CombinedOutput() + if len(out) != 0 { + t.Logf("\n----------\n%s----------\n", string(out)) + } + assert.NoError(t, err, "Running webdav integration tests") +}