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