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 }