github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/cmd/serve/webdav/webdav.go (about)

     1  //+build go1.9
     2  
     3  package webdav
     4  
     5  import (
     6  	"context"
     7  	"net/http"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/ncw/rclone/cmd"
    12  	"github.com/ncw/rclone/cmd/serve/httplib"
    13  	"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
    14  	"github.com/ncw/rclone/cmd/serve/httplib/serve"
    15  	"github.com/ncw/rclone/fs"
    16  	"github.com/ncw/rclone/fs/hash"
    17  	"github.com/ncw/rclone/fs/log"
    18  	"github.com/ncw/rclone/vfs"
    19  	"github.com/ncw/rclone/vfs/vfsflags"
    20  	"github.com/spf13/cobra"
    21  	"golang.org/x/net/webdav"
    22  )
    23  
    24  var (
    25  	hashName      string
    26  	hashType      = hash.None
    27  	disableGETDir = false
    28  )
    29  
    30  func init() {
    31  	httpflags.AddFlags(Command.Flags())
    32  	vfsflags.AddFlags(Command.Flags())
    33  	Command.Flags().StringVar(&hashName, "etag-hash", "", "Which hash to use for the ETag, or auto or blank for off")
    34  	Command.Flags().BoolVar(&disableGETDir, "disable-dir-list", false, "Disable HTML directory list on GET request for a directory")
    35  }
    36  
    37  // Command definition for cobra
    38  var Command = &cobra.Command{
    39  	Use:   "webdav remote:path",
    40  	Short: `Serve remote:path over webdav.`,
    41  	Long: `
    42  rclone serve webdav implements a basic webdav server to serve the
    43  remote over HTTP via the webdav protocol. This can be viewed with a
    44  webdav client, through a web browser, or you can make a remote of
    45  type webdav to read and write it.
    46  
    47  ### Webdav options
    48  
    49  #### --etag-hash 
    50  
    51  This controls the ETag header.  Without this flag the ETag will be
    52  based on the ModTime and Size of the object.
    53  
    54  If this flag is set to "auto" then rclone will choose the first
    55  supported hash on the backend or you can use a named hash such as
    56  "MD5" or "SHA-1".
    57  
    58  Use "rclone hashsum" to see the full list.
    59  
    60  ` + httplib.Help + vfs.Help,
    61  	RunE: func(command *cobra.Command, args []string) error {
    62  		cmd.CheckArgs(1, 1, command, args)
    63  		f := cmd.NewFsSrc(args)
    64  		hashType = hash.None
    65  		if hashName == "auto" {
    66  			hashType = f.Hashes().GetOne()
    67  		} else if hashName != "" {
    68  			err := hashType.Set(hashName)
    69  			if err != nil {
    70  				return err
    71  			}
    72  		}
    73  		if hashType != hash.None {
    74  			fs.Debugf(f, "Using hash %v for ETag", hashType)
    75  		}
    76  		cmd.Run(false, false, command, func() error {
    77  			s := newWebDAV(f, &httpflags.Opt)
    78  			err := s.serve()
    79  			if err != nil {
    80  				return err
    81  			}
    82  			s.Wait()
    83  			return nil
    84  		})
    85  		return nil
    86  	},
    87  }
    88  
    89  // WebDAV is a webdav.FileSystem interface
    90  //
    91  // A FileSystem implements access to a collection of named files. The elements
    92  // in a file path are separated by slash ('/', U+002F) characters, regardless
    93  // of host operating system convention.
    94  //
    95  // Each method has the same semantics as the os package's function of the same
    96  // name.
    97  //
    98  // Note that the os.Rename documentation says that "OS-specific restrictions
    99  // might apply". In particular, whether or not renaming a file or directory
   100  // overwriting another existing file or directory is an error is OS-dependent.
   101  type WebDAV struct {
   102  	*httplib.Server
   103  	f             fs.Fs
   104  	vfs           *vfs.VFS
   105  	webdavhandler *webdav.Handler
   106  }
   107  
   108  // check interface
   109  var _ webdav.FileSystem = (*WebDAV)(nil)
   110  
   111  // Make a new WebDAV to serve the remote
   112  func newWebDAV(f fs.Fs, opt *httplib.Options) *WebDAV {
   113  	w := &WebDAV{
   114  		f:   f,
   115  		vfs: vfs.New(f, &vfsflags.Opt),
   116  	}
   117  	webdavHandler := &webdav.Handler{
   118  		FileSystem: w,
   119  		LockSystem: webdav.NewMemLS(),
   120  		Logger:     w.logRequest, // FIXME
   121  	}
   122  	w.webdavhandler = webdavHandler
   123  	w.Server = httplib.NewServer(http.HandlerFunc(w.handler), opt)
   124  	return w
   125  }
   126  
   127  func (w *WebDAV) handler(rw http.ResponseWriter, r *http.Request) {
   128  	urlPath := r.URL.Path
   129  	isDir := strings.HasSuffix(urlPath, "/")
   130  	remote := strings.Trim(urlPath, "/")
   131  	if !disableGETDir && (r.Method == "GET" || r.Method == "HEAD") && isDir {
   132  		w.serveDir(rw, r, remote)
   133  		return
   134  	}
   135  	w.webdavhandler.ServeHTTP(rw, r)
   136  }
   137  
   138  // serveDir serves a directory index at dirRemote
   139  // This is similar to serveDir in serve http.
   140  func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote string) {
   141  	// List the directory
   142  	node, err := w.vfs.Stat(dirRemote)
   143  	if err == vfs.ENOENT {
   144  		http.Error(rw, "Directory not found", http.StatusNotFound)
   145  		return
   146  	} else if err != nil {
   147  		serve.Error(dirRemote, rw, "Failed to list directory", err)
   148  		return
   149  	}
   150  	if !node.IsDir() {
   151  		http.Error(rw, "Not a directory", http.StatusNotFound)
   152  		return
   153  	}
   154  	dir := node.(*vfs.Dir)
   155  	dirEntries, err := dir.ReadDirAll()
   156  	if err != nil {
   157  		serve.Error(dirRemote, rw, "Failed to list directory", err)
   158  		return
   159  	}
   160  
   161  	// Make the entries for display
   162  	directory := serve.NewDirectory(dirRemote, w.HTMLTemplate)
   163  	for _, node := range dirEntries {
   164  		directory.AddEntry(node.Path(), node.IsDir())
   165  	}
   166  
   167  	directory.Serve(rw, r)
   168  }
   169  
   170  // serve runs the http server in the background.
   171  //
   172  // Use s.Close() and s.Wait() to shutdown server
   173  func (w *WebDAV) serve() error {
   174  	err := w.Serve()
   175  	if err != nil {
   176  		return err
   177  	}
   178  	fs.Logf(w.f, "WebDav Server started on %s", w.URL())
   179  	return nil
   180  }
   181  
   182  // logRequest is called by the webdav module on every request
   183  func (w *WebDAV) logRequest(r *http.Request, err error) {
   184  	fs.Infof(r.URL.Path, "%s from %s", r.Method, r.RemoteAddr)
   185  }
   186  
   187  // Mkdir creates a directory
   188  func (w *WebDAV) Mkdir(ctx context.Context, name string, perm os.FileMode) (err error) {
   189  	defer log.Trace(name, "perm=%v", perm)("err = %v", &err)
   190  	dir, leaf, err := w.vfs.StatParent(name)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	_, err = dir.Mkdir(leaf)
   195  	return err
   196  }
   197  
   198  // OpenFile opens a file or a directory
   199  func (w *WebDAV) OpenFile(ctx context.Context, name string, flags int, perm os.FileMode) (file webdav.File, err error) {
   200  	defer log.Trace(name, "flags=%v, perm=%v", flags, perm)("err = %v", &err)
   201  	f, err := w.vfs.OpenFile(name, flags, perm)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  	return Handle{f}, nil
   206  }
   207  
   208  // RemoveAll removes a file or a directory and its contents
   209  func (w *WebDAV) RemoveAll(ctx context.Context, name string) (err error) {
   210  	defer log.Trace(name, "")("err = %v", &err)
   211  	node, err := w.vfs.Stat(name)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	err = node.RemoveAll()
   216  	if err != nil {
   217  		return err
   218  	}
   219  	return nil
   220  }
   221  
   222  // Rename a file or a directory
   223  func (w *WebDAV) Rename(ctx context.Context, oldName, newName string) (err error) {
   224  	defer log.Trace(oldName, "newName=%q", newName)("err = %v", &err)
   225  	return w.vfs.Rename(oldName, newName)
   226  }
   227  
   228  // Stat returns info about the file or directory
   229  func (w *WebDAV) Stat(ctx context.Context, name string) (fi os.FileInfo, err error) {
   230  	defer log.Trace(name, "")("fi=%+v, err = %v", &fi, &err)
   231  	fi, err = w.vfs.Stat(name)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	return FileInfo{fi}, nil
   236  }
   237  
   238  // Handle represents an open file
   239  type Handle struct {
   240  	vfs.Handle
   241  }
   242  
   243  // Readdir reads directory entries from the handle
   244  func (h Handle) Readdir(count int) (fis []os.FileInfo, err error) {
   245  	fis, err = h.Handle.Readdir(count)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  	// Wrap each FileInfo
   250  	for i := range fis {
   251  		fis[i] = FileInfo{fis[i]}
   252  	}
   253  	return fis, nil
   254  }
   255  
   256  // Stat the handle
   257  func (h Handle) Stat() (fi os.FileInfo, err error) {
   258  	fi, err = h.Handle.Stat()
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	return FileInfo{fi}, nil
   263  }
   264  
   265  // FileInfo represents info about a file satisfying os.FileInfo and
   266  // also some additional interfaces for webdav for ETag and ContentType
   267  type FileInfo struct {
   268  	os.FileInfo
   269  }
   270  
   271  // ETag returns an ETag for the FileInfo
   272  func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) {
   273  	defer log.Trace(fi, "")("etag=%q, err=%v", &etag, &err)
   274  	if hashType == hash.None {
   275  		return "", webdav.ErrNotImplemented
   276  	}
   277  	node, ok := (fi.FileInfo).(vfs.Node)
   278  	if !ok {
   279  		fs.Errorf(fi, "Expecting vfs.Node, got %T", fi.FileInfo)
   280  		return "", webdav.ErrNotImplemented
   281  	}
   282  	entry := node.DirEntry()
   283  	o, ok := entry.(fs.Object)
   284  	if !ok {
   285  		return "", webdav.ErrNotImplemented
   286  	}
   287  	hash, err := o.Hash(ctx, hashType)
   288  	if err != nil || hash == "" {
   289  		return "", webdav.ErrNotImplemented
   290  	}
   291  	return `"` + hash + `"`, nil
   292  }
   293  
   294  // ContentType returns a content type for the FileInfo
   295  func (fi FileInfo) ContentType(ctx context.Context) (contentType string, err error) {
   296  	defer log.Trace(fi, "")("etag=%q, err=%v", &contentType, &err)
   297  	node, ok := (fi.FileInfo).(vfs.Node)
   298  	if !ok {
   299  		fs.Errorf(fi, "Expecting vfs.Node, got %T", fi.FileInfo)
   300  		return "application/octet-stream", nil
   301  	}
   302  	entry := node.DirEntry()
   303  	switch x := entry.(type) {
   304  	case fs.Object:
   305  		return fs.MimeType(ctx, x), nil
   306  	case fs.Directory:
   307  		return "inode/directory", nil
   308  	}
   309  	fs.Errorf(fi, "Expecting fs.Object or fs.Directory, got %T", entry)
   310  	return "application/octet-stream", nil
   311  }