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