github.com/Andyfoo/golang/x/net@v0.0.0-20190901054642-57c1bf301704/webdav/webdav.go (about) 1 // Copyright 2014 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package webdav provides a WebDAV server implementation. 6 package webdav // import "github.com/Andyfoo/golang/x/net/webdav" 7 8 import ( 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "net/url" 14 "os" 15 "path" 16 "strings" 17 "time" 18 ) 19 20 type Handler struct { 21 // Prefix is the URL path prefix to strip from WebDAV resource paths. 22 Prefix string 23 // FileSystem is the virtual file system. 24 FileSystem FileSystem 25 // LockSystem is the lock management system. 26 LockSystem LockSystem 27 // Logger is an optional error logger. If non-nil, it will be called 28 // for all HTTP requests. 29 Logger func(*http.Request, error) 30 } 31 32 func (h *Handler) stripPrefix(p string) (string, int, error) { 33 if h.Prefix == "" { 34 return p, http.StatusOK, nil 35 } 36 if r := strings.TrimPrefix(p, h.Prefix); len(r) < len(p) { 37 return r, http.StatusOK, nil 38 } 39 return p, http.StatusNotFound, errPrefixMismatch 40 } 41 42 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 43 status, err := http.StatusBadRequest, errUnsupportedMethod 44 if h.FileSystem == nil { 45 status, err = http.StatusInternalServerError, errNoFileSystem 46 } else if h.LockSystem == nil { 47 status, err = http.StatusInternalServerError, errNoLockSystem 48 } else { 49 switch r.Method { 50 case "OPTIONS": 51 status, err = h.handleOptions(w, r) 52 case "GET", "HEAD", "POST": 53 status, err = h.handleGetHeadPost(w, r) 54 case "DELETE": 55 status, err = h.handleDelete(w, r) 56 case "PUT": 57 status, err = h.handlePut(w, r) 58 case "MKCOL": 59 status, err = h.handleMkcol(w, r) 60 case "COPY", "MOVE": 61 status, err = h.handleCopyMove(w, r) 62 case "LOCK": 63 status, err = h.handleLock(w, r) 64 case "UNLOCK": 65 status, err = h.handleUnlock(w, r) 66 case "PROPFIND": 67 status, err = h.handlePropfind(w, r) 68 case "PROPPATCH": 69 status, err = h.handleProppatch(w, r) 70 } 71 } 72 73 if status != 0 { 74 w.WriteHeader(status) 75 if status != http.StatusNoContent { 76 w.Write([]byte(StatusText(status))) 77 } 78 } 79 if h.Logger != nil { 80 h.Logger(r, err) 81 } 82 } 83 84 func (h *Handler) lock(now time.Time, root string) (token string, status int, err error) { 85 token, err = h.LockSystem.Create(now, LockDetails{ 86 Root: root, 87 Duration: infiniteTimeout, 88 ZeroDepth: true, 89 }) 90 if err != nil { 91 if err == ErrLocked { 92 return "", StatusLocked, err 93 } 94 return "", http.StatusInternalServerError, err 95 } 96 return token, 0, nil 97 } 98 99 func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) { 100 hdr := r.Header.Get("If") 101 if hdr == "" { 102 // An empty If header means that the client hasn't previously created locks. 103 // Even if this client doesn't care about locks, we still need to check that 104 // the resources aren't locked by another client, so we create temporary 105 // locks that would conflict with another client's locks. These temporary 106 // locks are unlocked at the end of the HTTP request. 107 now, srcToken, dstToken := time.Now(), "", "" 108 if src != "" { 109 srcToken, status, err = h.lock(now, src) 110 if err != nil { 111 return nil, status, err 112 } 113 } 114 if dst != "" { 115 dstToken, status, err = h.lock(now, dst) 116 if err != nil { 117 if srcToken != "" { 118 h.LockSystem.Unlock(now, srcToken) 119 } 120 return nil, status, err 121 } 122 } 123 124 return func() { 125 if dstToken != "" { 126 h.LockSystem.Unlock(now, dstToken) 127 } 128 if srcToken != "" { 129 h.LockSystem.Unlock(now, srcToken) 130 } 131 }, 0, nil 132 } 133 134 ih, ok := parseIfHeader(hdr) 135 if !ok { 136 return nil, http.StatusBadRequest, errInvalidIfHeader 137 } 138 // ih is a disjunction (OR) of ifLists, so any ifList will do. 139 for _, l := range ih.lists { 140 lsrc := l.resourceTag 141 if lsrc == "" { 142 lsrc = src 143 } else { 144 u, err := url.Parse(lsrc) 145 if err != nil { 146 continue 147 } 148 if u.Host != r.Host { 149 continue 150 } 151 lsrc, status, err = h.stripPrefix(u.Path) 152 if err != nil { 153 return nil, status, err 154 } 155 } 156 release, err = h.LockSystem.Confirm(time.Now(), lsrc, dst, l.conditions...) 157 if err == ErrConfirmationFailed { 158 continue 159 } 160 if err != nil { 161 return nil, http.StatusInternalServerError, err 162 } 163 return release, 0, nil 164 } 165 // Section 10.4.1 says that "If this header is evaluated and all state lists 166 // fail, then the request must fail with a 412 (Precondition Failed) status." 167 // We follow the spec even though the cond_put_corrupt_token test case from 168 // the litmus test warns on seeing a 412 instead of a 423 (Locked). 169 return nil, http.StatusPreconditionFailed, ErrLocked 170 } 171 172 func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) { 173 reqPath, status, err := h.stripPrefix(r.URL.Path) 174 if err != nil { 175 return status, err 176 } 177 ctx := r.Context() 178 allow := "OPTIONS, LOCK, PUT, MKCOL" 179 if fi, err := h.FileSystem.Stat(ctx, reqPath); err == nil { 180 if fi.IsDir() { 181 allow = "OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND" 182 } else { 183 allow = "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT" 184 } 185 } 186 w.Header().Set("Allow", allow) 187 // http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes 188 w.Header().Set("DAV", "1, 2") 189 // http://msdn.microsoft.com/en-au/library/cc250217.aspx 190 w.Header().Set("MS-Author-Via", "DAV") 191 return 0, nil 192 } 193 194 func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) { 195 reqPath, status, err := h.stripPrefix(r.URL.Path) 196 if err != nil { 197 return status, err 198 } 199 // TODO: check locks for read-only access?? 200 ctx := r.Context() 201 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDONLY, 0) 202 if err != nil { 203 return http.StatusNotFound, err 204 } 205 defer f.Close() 206 fi, err := f.Stat() 207 if err != nil { 208 return http.StatusNotFound, err 209 } 210 if fi.IsDir() { 211 return http.StatusMethodNotAllowed, nil 212 } 213 etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi) 214 if err != nil { 215 return http.StatusInternalServerError, err 216 } 217 w.Header().Set("ETag", etag) 218 // Let ServeContent determine the Content-Type header. 219 http.ServeContent(w, r, reqPath, fi.ModTime(), f) 220 return 0, nil 221 } 222 223 func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) { 224 reqPath, status, err := h.stripPrefix(r.URL.Path) 225 if err != nil { 226 return status, err 227 } 228 release, status, err := h.confirmLocks(r, reqPath, "") 229 if err != nil { 230 return status, err 231 } 232 defer release() 233 234 ctx := r.Context() 235 236 // TODO: return MultiStatus where appropriate. 237 238 // "godoc os RemoveAll" says that "If the path does not exist, RemoveAll 239 // returns nil (no error)." WebDAV semantics are that it should return a 240 // "404 Not Found". We therefore have to Stat before we RemoveAll. 241 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil { 242 if os.IsNotExist(err) { 243 return http.StatusNotFound, err 244 } 245 return http.StatusMethodNotAllowed, err 246 } 247 if err := h.FileSystem.RemoveAll(ctx, reqPath); err != nil { 248 return http.StatusMethodNotAllowed, err 249 } 250 return http.StatusNoContent, nil 251 } 252 253 func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) { 254 reqPath, status, err := h.stripPrefix(r.URL.Path) 255 if err != nil { 256 return status, err 257 } 258 release, status, err := h.confirmLocks(r, reqPath, "") 259 if err != nil { 260 return status, err 261 } 262 defer release() 263 // TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz' 264 // comments in http.checkEtag. 265 ctx := r.Context() 266 267 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 268 if err != nil { 269 return http.StatusNotFound, err 270 } 271 _, copyErr := io.Copy(f, r.Body) 272 fi, statErr := f.Stat() 273 closeErr := f.Close() 274 // TODO(rost): Returning 405 Method Not Allowed might not be appropriate. 275 if copyErr != nil { 276 return http.StatusMethodNotAllowed, copyErr 277 } 278 if statErr != nil { 279 return http.StatusMethodNotAllowed, statErr 280 } 281 if closeErr != nil { 282 return http.StatusMethodNotAllowed, closeErr 283 } 284 etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi) 285 if err != nil { 286 return http.StatusInternalServerError, err 287 } 288 w.Header().Set("ETag", etag) 289 return http.StatusCreated, nil 290 } 291 292 func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) { 293 reqPath, status, err := h.stripPrefix(r.URL.Path) 294 if err != nil { 295 return status, err 296 } 297 release, status, err := h.confirmLocks(r, reqPath, "") 298 if err != nil { 299 return status, err 300 } 301 defer release() 302 303 ctx := r.Context() 304 305 if r.ContentLength > 0 { 306 return http.StatusUnsupportedMediaType, nil 307 } 308 if err := h.FileSystem.Mkdir(ctx, reqPath, 0777); err != nil { 309 if os.IsNotExist(err) { 310 return http.StatusConflict, err 311 } 312 return http.StatusMethodNotAllowed, err 313 } 314 return http.StatusCreated, nil 315 } 316 317 func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) { 318 hdr := r.Header.Get("Destination") 319 if hdr == "" { 320 return http.StatusBadRequest, errInvalidDestination 321 } 322 u, err := url.Parse(hdr) 323 if err != nil { 324 return http.StatusBadRequest, errInvalidDestination 325 } 326 if u.Host != r.Host { 327 return http.StatusBadGateway, errInvalidDestination 328 } 329 330 src, status, err := h.stripPrefix(r.URL.Path) 331 if err != nil { 332 return status, err 333 } 334 335 dst, status, err := h.stripPrefix(u.Path) 336 if err != nil { 337 return status, err 338 } 339 340 if dst == "" { 341 return http.StatusBadGateway, errInvalidDestination 342 } 343 if dst == src { 344 return http.StatusForbidden, errDestinationEqualsSource 345 } 346 347 ctx := r.Context() 348 349 if r.Method == "COPY" { 350 // Section 7.5.1 says that a COPY only needs to lock the destination, 351 // not both destination and source. Strictly speaking, this is racy, 352 // even though a COPY doesn't modify the source, if a concurrent 353 // operation modifies the source. However, the litmus test explicitly 354 // checks that COPYing a locked-by-another source is OK. 355 release, status, err := h.confirmLocks(r, "", dst) 356 if err != nil { 357 return status, err 358 } 359 defer release() 360 361 // Section 9.8.3 says that "The COPY method on a collection without a Depth 362 // header must act as if a Depth header with value "infinity" was included". 363 depth := infiniteDepth 364 if hdr := r.Header.Get("Depth"); hdr != "" { 365 depth = parseDepth(hdr) 366 if depth != 0 && depth != infiniteDepth { 367 // Section 9.8.3 says that "A client may submit a Depth header on a 368 // COPY on a collection with a value of "0" or "infinity"." 369 return http.StatusBadRequest, errInvalidDepth 370 } 371 } 372 return copyFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") != "F", depth, 0) 373 } 374 375 release, status, err := h.confirmLocks(r, src, dst) 376 if err != nil { 377 return status, err 378 } 379 defer release() 380 381 // Section 9.9.2 says that "The MOVE method on a collection must act as if 382 // a "Depth: infinity" header was used on it. A client must not submit a 383 // Depth header on a MOVE on a collection with any value but "infinity"." 384 if hdr := r.Header.Get("Depth"); hdr != "" { 385 if parseDepth(hdr) != infiniteDepth { 386 return http.StatusBadRequest, errInvalidDepth 387 } 388 } 389 return moveFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") == "T") 390 } 391 392 func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) { 393 duration, err := parseTimeout(r.Header.Get("Timeout")) 394 if err != nil { 395 return http.StatusBadRequest, err 396 } 397 li, status, err := readLockInfo(r.Body) 398 if err != nil { 399 return status, err 400 } 401 402 ctx := r.Context() 403 token, ld, now, created := "", LockDetails{}, time.Now(), false 404 if li == (lockInfo{}) { 405 // An empty lockInfo means to refresh the lock. 406 ih, ok := parseIfHeader(r.Header.Get("If")) 407 if !ok { 408 return http.StatusBadRequest, errInvalidIfHeader 409 } 410 if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { 411 token = ih.lists[0].conditions[0].Token 412 } 413 if token == "" { 414 return http.StatusBadRequest, errInvalidLockToken 415 } 416 ld, err = h.LockSystem.Refresh(now, token, duration) 417 if err != nil { 418 if err == ErrNoSuchLock { 419 return http.StatusPreconditionFailed, err 420 } 421 return http.StatusInternalServerError, err 422 } 423 424 } else { 425 // Section 9.10.3 says that "If no Depth header is submitted on a LOCK request, 426 // then the request MUST act as if a "Depth:infinity" had been submitted." 427 depth := infiniteDepth 428 if hdr := r.Header.Get("Depth"); hdr != "" { 429 depth = parseDepth(hdr) 430 if depth != 0 && depth != infiniteDepth { 431 // Section 9.10.3 says that "Values other than 0 or infinity must not be 432 // used with the Depth header on a LOCK method". 433 return http.StatusBadRequest, errInvalidDepth 434 } 435 } 436 reqPath, status, err := h.stripPrefix(r.URL.Path) 437 if err != nil { 438 return status, err 439 } 440 ld = LockDetails{ 441 Root: reqPath, 442 Duration: duration, 443 OwnerXML: li.Owner.InnerXML, 444 ZeroDepth: depth == 0, 445 } 446 token, err = h.LockSystem.Create(now, ld) 447 if err != nil { 448 if err == ErrLocked { 449 return StatusLocked, err 450 } 451 return http.StatusInternalServerError, err 452 } 453 defer func() { 454 if retErr != nil { 455 h.LockSystem.Unlock(now, token) 456 } 457 }() 458 459 // Create the resource if it didn't previously exist. 460 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil { 461 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 462 if err != nil { 463 // TODO: detect missing intermediate dirs and return http.StatusConflict? 464 return http.StatusInternalServerError, err 465 } 466 f.Close() 467 created = true 468 } 469 470 // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the 471 // Lock-Token value is a Coded-URL. We add angle brackets. 472 w.Header().Set("Lock-Token", "<"+token+">") 473 } 474 475 w.Header().Set("Content-Type", "application/xml; charset=utf-8") 476 if created { 477 // This is "w.WriteHeader(http.StatusCreated)" and not "return 478 // http.StatusCreated, nil" because we write our own (XML) response to w 479 // and Handler.ServeHTTP would otherwise write "Created". 480 w.WriteHeader(http.StatusCreated) 481 } 482 writeLockInfo(w, token, ld) 483 return 0, nil 484 } 485 486 func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) { 487 // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the 488 // Lock-Token value is a Coded-URL. We strip its angle brackets. 489 t := r.Header.Get("Lock-Token") 490 if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' { 491 return http.StatusBadRequest, errInvalidLockToken 492 } 493 t = t[1 : len(t)-1] 494 495 switch err = h.LockSystem.Unlock(time.Now(), t); err { 496 case nil: 497 return http.StatusNoContent, err 498 case ErrForbidden: 499 return http.StatusForbidden, err 500 case ErrLocked: 501 return StatusLocked, err 502 case ErrNoSuchLock: 503 return http.StatusConflict, err 504 default: 505 return http.StatusInternalServerError, err 506 } 507 } 508 509 func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) { 510 reqPath, status, err := h.stripPrefix(r.URL.Path) 511 if err != nil { 512 return status, err 513 } 514 ctx := r.Context() 515 fi, err := h.FileSystem.Stat(ctx, reqPath) 516 if err != nil { 517 if os.IsNotExist(err) { 518 return http.StatusNotFound, err 519 } 520 return http.StatusMethodNotAllowed, err 521 } 522 depth := infiniteDepth 523 if hdr := r.Header.Get("Depth"); hdr != "" { 524 depth = parseDepth(hdr) 525 if depth == invalidDepth { 526 return http.StatusBadRequest, errInvalidDepth 527 } 528 } 529 pf, status, err := readPropfind(r.Body) 530 if err != nil { 531 return status, err 532 } 533 534 mw := multistatusWriter{w: w} 535 536 walkFn := func(reqPath string, info os.FileInfo, err error) error { 537 if err != nil { 538 return err 539 } 540 var pstats []Propstat 541 if pf.Propname != nil { 542 pnames, err := propnames(ctx, h.FileSystem, h.LockSystem, reqPath) 543 if err != nil { 544 return err 545 } 546 pstat := Propstat{Status: http.StatusOK} 547 for _, xmlname := range pnames { 548 pstat.Props = append(pstat.Props, Property{XMLName: xmlname}) 549 } 550 pstats = append(pstats, pstat) 551 } else if pf.Allprop != nil { 552 pstats, err = allprop(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop) 553 } else { 554 pstats, err = props(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop) 555 } 556 if err != nil { 557 return err 558 } 559 href := path.Join(h.Prefix, reqPath) 560 if href != "/" && info.IsDir() { 561 href += "/" 562 } 563 return mw.write(makePropstatResponse(href, pstats)) 564 } 565 566 walkErr := walkFS(ctx, h.FileSystem, depth, reqPath, fi, walkFn) 567 closeErr := mw.close() 568 if walkErr != nil { 569 return http.StatusInternalServerError, walkErr 570 } 571 if closeErr != nil { 572 return http.StatusInternalServerError, closeErr 573 } 574 return 0, nil 575 } 576 577 func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) { 578 reqPath, status, err := h.stripPrefix(r.URL.Path) 579 if err != nil { 580 return status, err 581 } 582 release, status, err := h.confirmLocks(r, reqPath, "") 583 if err != nil { 584 return status, err 585 } 586 defer release() 587 588 ctx := r.Context() 589 590 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil { 591 if os.IsNotExist(err) { 592 return http.StatusNotFound, err 593 } 594 return http.StatusMethodNotAllowed, err 595 } 596 patches, status, err := readProppatch(r.Body) 597 if err != nil { 598 return status, err 599 } 600 pstats, err := patch(ctx, h.FileSystem, h.LockSystem, reqPath, patches) 601 if err != nil { 602 return http.StatusInternalServerError, err 603 } 604 mw := multistatusWriter{w: w} 605 writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats)) 606 closeErr := mw.close() 607 if writeErr != nil { 608 return http.StatusInternalServerError, writeErr 609 } 610 if closeErr != nil { 611 return http.StatusInternalServerError, closeErr 612 } 613 return 0, nil 614 } 615 616 func makePropstatResponse(href string, pstats []Propstat) *response { 617 resp := response{ 618 Href: []string{(&url.URL{Path: href}).EscapedPath()}, 619 Propstat: make([]propstat, 0, len(pstats)), 620 } 621 for _, p := range pstats { 622 var xmlErr *xmlError 623 if p.XMLError != "" { 624 xmlErr = &xmlError{InnerXML: []byte(p.XMLError)} 625 } 626 resp.Propstat = append(resp.Propstat, propstat{ 627 Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)), 628 Prop: p.Props, 629 ResponseDescription: p.ResponseDescription, 630 Error: xmlErr, 631 }) 632 } 633 return &resp 634 } 635 636 const ( 637 infiniteDepth = -1 638 invalidDepth = -2 639 ) 640 641 // parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and 642 // infiniteDepth. Parsing any other string returns invalidDepth. 643 // 644 // Different WebDAV methods have further constraints on valid depths: 645 // - PROPFIND has no further restrictions, as per section 9.1. 646 // - COPY accepts only "0" or "infinity", as per section 9.8.3. 647 // - MOVE accepts only "infinity", as per section 9.9.2. 648 // - LOCK accepts only "0" or "infinity", as per section 9.10.3. 649 // These constraints are enforced by the handleXxx methods. 650 func parseDepth(s string) int { 651 switch s { 652 case "0": 653 return 0 654 case "1": 655 return 1 656 case "infinity": 657 return infiniteDepth 658 } 659 return invalidDepth 660 } 661 662 // http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11 663 const ( 664 StatusMulti = 207 665 StatusUnprocessableEntity = 422 666 StatusLocked = 423 667 StatusFailedDependency = 424 668 StatusInsufficientStorage = 507 669 ) 670 671 func StatusText(code int) string { 672 switch code { 673 case StatusMulti: 674 return "Multi-Status" 675 case StatusUnprocessableEntity: 676 return "Unprocessable Entity" 677 case StatusLocked: 678 return "Locked" 679 case StatusFailedDependency: 680 return "Failed Dependency" 681 case StatusInsufficientStorage: 682 return "Insufficient Storage" 683 } 684 return http.StatusText(code) 685 } 686 687 var ( 688 errDestinationEqualsSource = errors.New("webdav: destination equals source") 689 errDirectoryNotEmpty = errors.New("webdav: directory not empty") 690 errInvalidDepth = errors.New("webdav: invalid depth") 691 errInvalidDestination = errors.New("webdav: invalid destination") 692 errInvalidIfHeader = errors.New("webdav: invalid If header") 693 errInvalidLockInfo = errors.New("webdav: invalid lock info") 694 errInvalidLockToken = errors.New("webdav: invalid lock token") 695 errInvalidPropfind = errors.New("webdav: invalid propfind") 696 errInvalidProppatch = errors.New("webdav: invalid proppatch") 697 errInvalidResponse = errors.New("webdav: invalid response") 698 errInvalidTimeout = errors.New("webdav: invalid timeout") 699 errNoFileSystem = errors.New("webdav: no file system") 700 errNoLockSystem = errors.New("webdav: no lock system") 701 errNotADirectory = errors.New("webdav: not a directory") 702 errPrefixMismatch = errors.New("webdav: prefix mismatch") 703 errRecursionTooDeep = errors.New("webdav: recursion too deep") 704 errUnsupportedLockInfo = errors.New("webdav: unsupported lock info") 705 errUnsupportedMethod = errors.New("webdav: unsupported method") 706 )