github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/backend/webdav/webdav.go (about) 1 // Package webdav provides an interface to the Webdav 2 // object storage system. 3 package webdav 4 5 // SetModTime might be possible 6 // https://stackoverflow.com/questions/3579608/webdav-can-a-client-modify-the-mtime-of-a-file 7 // ...support for a PROPSET to lastmodified (mind the missing get) which does the utime() call might be an option. 8 // For example the ownCloud WebDAV server does it that way. 9 10 import ( 11 "bytes" 12 "context" 13 "encoding/xml" 14 "fmt" 15 "io" 16 "net/http" 17 "net/url" 18 "os/exec" 19 "path" 20 "strings" 21 "time" 22 23 "github.com/pkg/errors" 24 "github.com/rclone/rclone/backend/webdav/api" 25 "github.com/rclone/rclone/backend/webdav/odrvcookie" 26 "github.com/rclone/rclone/fs" 27 "github.com/rclone/rclone/fs/config/configmap" 28 "github.com/rclone/rclone/fs/config/configstruct" 29 "github.com/rclone/rclone/fs/config/obscure" 30 "github.com/rclone/rclone/fs/fserrors" 31 "github.com/rclone/rclone/fs/fshttp" 32 "github.com/rclone/rclone/fs/hash" 33 "github.com/rclone/rclone/lib/pacer" 34 "github.com/rclone/rclone/lib/rest" 35 ) 36 37 const ( 38 minSleep = 10 * time.Millisecond 39 maxSleep = 2 * time.Second 40 decayConstant = 2 // bigger for slower decay, exponential 41 defaultDepth = "1" // depth for PROPFIND 42 ) 43 44 // Register with Fs 45 func init() { 46 fs.Register(&fs.RegInfo{ 47 Name: "webdav", 48 Description: "Webdav", 49 NewFs: NewFs, 50 Options: []fs.Option{{ 51 Name: "url", 52 Help: "URL of http host to connect to", 53 Required: true, 54 Examples: []fs.OptionExample{{ 55 Value: "https://example.com", 56 Help: "Connect to example.com", 57 }}, 58 }, { 59 Name: "vendor", 60 Help: "Name of the Webdav site/service/software you are using", 61 Examples: []fs.OptionExample{{ 62 Value: "nextcloud", 63 Help: "Nextcloud", 64 }, { 65 Value: "owncloud", 66 Help: "Owncloud", 67 }, { 68 Value: "sharepoint", 69 Help: "Sharepoint", 70 }, { 71 Value: "other", 72 Help: "Other site/service or software", 73 }}, 74 }, { 75 Name: "user", 76 Help: "User name", 77 }, { 78 Name: "pass", 79 Help: "Password.", 80 IsPassword: true, 81 }, { 82 Name: "bearer_token", 83 Help: "Bearer token instead of user/pass (eg a Macaroon)", 84 }, { 85 Name: "bearer_token_command", 86 Help: "Command to run to get a bearer token", 87 Advanced: true, 88 }}, 89 }) 90 } 91 92 // Options defines the configuration for this backend 93 type Options struct { 94 URL string `config:"url"` 95 Vendor string `config:"vendor"` 96 User string `config:"user"` 97 Pass string `config:"pass"` 98 BearerToken string `config:"bearer_token"` 99 BearerTokenCommand string `config:"bearer_token_command"` 100 } 101 102 // Fs represents a remote webdav 103 type Fs struct { 104 name string // name of this remote 105 root string // the path we are working on 106 opt Options // parsed options 107 features *fs.Features // optional features 108 endpoint *url.URL // URL of the host 109 endpointURL string // endpoint as a string 110 srv *rest.Client // the connection to the one drive server 111 pacer *fs.Pacer // pacer for API calls 112 precision time.Duration // mod time precision 113 canStream bool // set if can stream 114 useOCMtime bool // set if can use X-OC-Mtime 115 retryWithZeroDepth bool // some vendors (sharepoint) won't list files when Depth is 1 (our default) 116 hasMD5 bool // set if can use owncloud style checksums for MD5 117 hasSHA1 bool // set if can use owncloud style checksums for SHA1 118 } 119 120 // Object describes a webdav object 121 // 122 // Will definitely have info but maybe not meta 123 type Object struct { 124 fs *Fs // what this object is part of 125 remote string // The remote path 126 hasMetaData bool // whether info below has been set 127 size int64 // size of the object 128 modTime time.Time // modification time of the object 129 sha1 string // SHA-1 of the object content if known 130 md5 string // MD5 of the object content if known 131 } 132 133 // ------------------------------------------------------------ 134 135 // Name of the remote (as passed into NewFs) 136 func (f *Fs) Name() string { 137 return f.name 138 } 139 140 // Root of the remote (as passed into NewFs) 141 func (f *Fs) Root() string { 142 return f.root 143 } 144 145 // String converts this Fs to a string 146 func (f *Fs) String() string { 147 return fmt.Sprintf("webdav root '%s'", f.root) 148 } 149 150 // Features returns the optional features of this Fs 151 func (f *Fs) Features() *fs.Features { 152 return f.features 153 } 154 155 // retryErrorCodes is a slice of error codes that we will retry 156 var retryErrorCodes = []int{ 157 423, // Locked 158 429, // Too Many Requests. 159 500, // Internal Server Error 160 502, // Bad Gateway 161 503, // Service Unavailable 162 504, // Gateway Timeout 163 509, // Bandwidth Limit Exceeded 164 } 165 166 // shouldRetry returns a boolean as to whether this resp and err 167 // deserve to be retried. It returns the err as a convenience 168 func (f *Fs) shouldRetry(resp *http.Response, err error) (bool, error) { 169 // If we have a bearer token command and it has expired then refresh it 170 if f.opt.BearerTokenCommand != "" && resp != nil && resp.StatusCode == 401 { 171 fs.Debugf(f, "Bearer token expired: %v", err) 172 authErr := f.fetchAndSetBearerToken() 173 if authErr != nil { 174 err = authErr 175 } 176 return true, err 177 } 178 return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 179 } 180 181 // itemIsDir returns true if the item is a directory 182 // 183 // When a client sees a resourcetype it doesn't recognize it should 184 // assume it is a regular non-collection resource. [WebDav book by 185 // Lisa Dusseault ch 7.5.8 p170] 186 func itemIsDir(item *api.Response) bool { 187 if t := item.Props.Type; t != nil { 188 if t.Space == "DAV:" && t.Local == "collection" { 189 return true 190 } 191 fs.Debugf(nil, "Unknown resource type %q/%q on %q", t.Space, t.Local, item.Props.Name) 192 } 193 // the iscollection prop is a Microsoft extension, but if present it is a reliable indicator 194 // if the above check failed - see #2716. This can be an integer or a boolean - see #2964 195 if t := item.Props.IsCollection; t != nil { 196 switch x := strings.ToLower(*t); x { 197 case "0", "false": 198 return false 199 case "1", "true": 200 return true 201 default: 202 fs.Debugf(nil, "Unknown value %q for IsCollection", x) 203 } 204 } 205 return false 206 } 207 208 // readMetaDataForPath reads the metadata from the path 209 func (f *Fs) readMetaDataForPath(ctx context.Context, path string, depth string) (info *api.Prop, err error) { 210 // FIXME how do we read back additional properties? 211 opts := rest.Opts{ 212 Method: "PROPFIND", 213 Path: f.filePath(path), 214 ExtraHeaders: map[string]string{ 215 "Depth": depth, 216 }, 217 NoRedirect: true, 218 } 219 if f.hasMD5 || f.hasSHA1 { 220 opts.Body = bytes.NewBuffer(owncloudProps) 221 } 222 var result api.Multistatus 223 var resp *http.Response 224 err = f.pacer.Call(func() (bool, error) { 225 resp, err = f.srv.CallXML(ctx, &opts, nil, &result) 226 return f.shouldRetry(resp, err) 227 }) 228 if apiErr, ok := err.(*api.Error); ok { 229 // does not exist 230 switch apiErr.StatusCode { 231 case http.StatusNotFound: 232 if f.retryWithZeroDepth && depth != "0" { 233 return f.readMetaDataForPath(ctx, path, "0") 234 } 235 return nil, fs.ErrorObjectNotFound 236 case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther: 237 // Some sort of redirect - go doesn't deal with these properly (it resets 238 // the method to GET). However we can assume that if it was redirected the 239 // object was not found. 240 return nil, fs.ErrorObjectNotFound 241 } 242 } 243 if err != nil { 244 return nil, errors.Wrap(err, "read metadata failed") 245 } 246 if len(result.Responses) < 1 { 247 return nil, fs.ErrorObjectNotFound 248 } 249 item := result.Responses[0] 250 if !item.Props.StatusOK() { 251 return nil, fs.ErrorObjectNotFound 252 } 253 if itemIsDir(&item) { 254 return nil, fs.ErrorNotAFile 255 } 256 return &item.Props, nil 257 } 258 259 // errorHandler parses a non 2xx error response into an error 260 func errorHandler(resp *http.Response) error { 261 body, err := rest.ReadBody(resp) 262 if err != nil { 263 return errors.Wrap(err, "error when trying to read error from body") 264 } 265 // Decode error response 266 errResponse := new(api.Error) 267 err = xml.Unmarshal(body, &errResponse) 268 if err != nil { 269 // set the Message to be the body if can't parse the XML 270 errResponse.Message = strings.TrimSpace(string(body)) 271 } 272 errResponse.Status = resp.Status 273 errResponse.StatusCode = resp.StatusCode 274 return errResponse 275 } 276 277 // addSlash makes sure s is terminated with a / if non empty 278 func addSlash(s string) string { 279 if s != "" && !strings.HasSuffix(s, "/") { 280 s += "/" 281 } 282 return s 283 } 284 285 // filePath returns a file path (f.root, file) 286 func (f *Fs) filePath(file string) string { 287 return rest.URLPathEscape(path.Join(f.root, file)) 288 } 289 290 // dirPath returns a directory path (f.root, dir) 291 func (f *Fs) dirPath(dir string) string { 292 return addSlash(f.filePath(dir)) 293 } 294 295 // filePath returns a file path (f.root, remote) 296 func (o *Object) filePath() string { 297 return o.fs.filePath(o.remote) 298 } 299 300 // NewFs constructs an Fs from the path, container:path 301 func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { 302 ctx := context.Background() 303 // Parse config into Options struct 304 opt := new(Options) 305 err := configstruct.Set(m, opt) 306 if err != nil { 307 return nil, err 308 } 309 rootIsDir := strings.HasSuffix(root, "/") 310 root = strings.Trim(root, "/") 311 312 if !strings.HasSuffix(opt.URL, "/") { 313 opt.URL += "/" 314 } 315 if opt.Pass != "" { 316 var err error 317 opt.Pass, err = obscure.Reveal(opt.Pass) 318 if err != nil { 319 return nil, errors.Wrap(err, "couldn't decrypt password") 320 } 321 } 322 if opt.Vendor == "" { 323 opt.Vendor = "other" 324 } 325 root = strings.Trim(root, "/") 326 327 // Parse the endpoint 328 u, err := url.Parse(opt.URL) 329 if err != nil { 330 return nil, err 331 } 332 333 f := &Fs{ 334 name: name, 335 root: root, 336 opt: *opt, 337 endpoint: u, 338 endpointURL: u.String(), 339 srv: rest.NewClient(fshttp.NewClient(fs.Config)).SetRoot(u.String()), 340 pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 341 precision: fs.ModTimeNotSupported, 342 } 343 f.features = (&fs.Features{ 344 CanHaveEmptyDirectories: true, 345 }).Fill(f) 346 if opt.User != "" || opt.Pass != "" { 347 f.srv.SetUserPass(opt.User, opt.Pass) 348 } else if opt.BearerToken != "" { 349 f.setBearerToken(opt.BearerToken) 350 } else if f.opt.BearerTokenCommand != "" { 351 err = f.fetchAndSetBearerToken() 352 if err != nil { 353 return nil, err 354 } 355 } 356 f.srv.SetErrorHandler(errorHandler) 357 err = f.setQuirks(ctx, opt.Vendor) 358 if err != nil { 359 return nil, err 360 } 361 f.srv.SetHeader("Referer", u.String()) 362 363 if root != "" && !rootIsDir { 364 // Check to see if the root actually an existing file 365 remote := path.Base(root) 366 f.root = path.Dir(root) 367 if f.root == "." { 368 f.root = "" 369 } 370 _, err := f.NewObject(ctx, remote) 371 if err != nil { 372 if errors.Cause(err) == fs.ErrorObjectNotFound || errors.Cause(err) == fs.ErrorNotAFile { 373 // File doesn't exist so return old f 374 f.root = root 375 return f, nil 376 } 377 return nil, err 378 } 379 // return an error with an fs which points to the parent 380 return f, fs.ErrorIsFile 381 } 382 return f, nil 383 } 384 385 // sets the BearerToken up 386 func (f *Fs) setBearerToken(token string) { 387 f.opt.BearerToken = token 388 f.srv.SetHeader("Authorization", "Bearer "+token) 389 } 390 391 // fetch the bearer token using the command 392 func (f *Fs) fetchBearerToken(cmd string) (string, error) { 393 var ( 394 args = strings.Split(cmd, " ") 395 stdout bytes.Buffer 396 stderr bytes.Buffer 397 c = exec.Command(args[0], args[1:]...) 398 ) 399 c.Stdout = &stdout 400 c.Stderr = &stderr 401 var ( 402 err = c.Run() 403 stdoutString = strings.TrimSpace(stdout.String()) 404 stderrString = strings.TrimSpace(stderr.String()) 405 ) 406 if err != nil { 407 if stderrString == "" { 408 stderrString = stdoutString 409 } 410 return "", errors.Wrapf(err, "failed to get bearer token using %q: %s", f.opt.BearerTokenCommand, stderrString) 411 } 412 return stdoutString, nil 413 } 414 415 // fetch the bearer token and set it if successful 416 func (f *Fs) fetchAndSetBearerToken() error { 417 if f.opt.BearerTokenCommand == "" { 418 return nil 419 } 420 token, err := f.fetchBearerToken(f.opt.BearerTokenCommand) 421 if err != nil { 422 return err 423 } 424 f.setBearerToken(token) 425 return nil 426 } 427 428 // setQuirks adjusts the Fs for the vendor passed in 429 func (f *Fs) setQuirks(ctx context.Context, vendor string) error { 430 switch vendor { 431 case "owncloud": 432 f.canStream = true 433 f.precision = time.Second 434 f.useOCMtime = true 435 f.hasMD5 = true 436 f.hasSHA1 = true 437 case "nextcloud": 438 f.precision = time.Second 439 f.useOCMtime = true 440 f.hasSHA1 = true 441 case "sharepoint": 442 // To mount sharepoint, two Cookies are required 443 // They have to be set instead of BasicAuth 444 f.srv.RemoveHeader("Authorization") // We don't need this Header if using cookies 445 spCk := odrvcookie.New(f.opt.User, f.opt.Pass, f.endpointURL) 446 spCookies, err := spCk.Cookies(ctx) 447 if err != nil { 448 return err 449 } 450 451 odrvcookie.NewRenew(12*time.Hour, func() { 452 spCookies, err := spCk.Cookies(ctx) 453 if err != nil { 454 fs.Errorf("could not renew cookies: %s", err.Error()) 455 return 456 } 457 f.srv.SetCookie(&spCookies.FedAuth, &spCookies.RtFa) 458 fs.Debugf(spCookies, "successfully renewed sharepoint cookies") 459 }) 460 461 f.srv.SetCookie(&spCookies.FedAuth, &spCookies.RtFa) 462 463 // sharepoint, unlike the other vendors, only lists files if the depth header is set to 0 464 // however, rclone defaults to 1 since it provides recursive directory listing 465 // to determine if we may have found a file, the request has to be resent 466 // with the depth set to 0 467 f.retryWithZeroDepth = true 468 case "other": 469 default: 470 fs.Debugf(f, "Unknown vendor %q", vendor) 471 } 472 473 // Remove PutStream from optional features 474 if !f.canStream { 475 f.features.PutStream = nil 476 } 477 return nil 478 } 479 480 // Return an Object from a path 481 // 482 // If it can't be found it returns the error fs.ErrorObjectNotFound. 483 func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.Prop) (fs.Object, error) { 484 o := &Object{ 485 fs: f, 486 remote: remote, 487 } 488 var err error 489 if info != nil { 490 // Set info 491 err = o.setMetaData(info) 492 } else { 493 err = o.readMetaData(ctx) // reads info and meta, returning an error 494 } 495 if err != nil { 496 return nil, err 497 } 498 return o, nil 499 } 500 501 // NewObject finds the Object at remote. If it can't be found 502 // it returns the error fs.ErrorObjectNotFound. 503 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 504 return f.newObjectWithInfo(ctx, remote, nil) 505 } 506 507 // Read the normal props, plus the checksums 508 // 509 // <oc:checksums><oc:checksum>SHA1:f572d396fae9206628714fb2ce00f72e94f2258f MD5:b1946ac92492d2347c6235b4d2611184 ADLER32:084b021f</oc:checksum></oc:checksums> 510 var owncloudProps = []byte(`<?xml version="1.0"?> 511 <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> 512 <d:prop> 513 <d:displayname /> 514 <d:getlastmodified /> 515 <d:getcontentlength /> 516 <d:resourcetype /> 517 <d:getcontenttype /> 518 <oc:checksums /> 519 </d:prop> 520 </d:propfind> 521 `) 522 523 // list the objects into the function supplied 524 // 525 // If directories is set it only sends directories 526 // User function to process a File item from listAll 527 // 528 // Should return true to finish processing 529 type listAllFn func(string, bool, *api.Prop) bool 530 531 // Lists the directory required calling the user function on each item found 532 // 533 // If the user fn ever returns true then it early exits with found = true 534 func (f *Fs) listAll(ctx context.Context, dir string, directoriesOnly bool, filesOnly bool, depth string, fn listAllFn) (found bool, err error) { 535 opts := rest.Opts{ 536 Method: "PROPFIND", 537 Path: f.dirPath(dir), // FIXME Should not start with / 538 ExtraHeaders: map[string]string{ 539 "Depth": depth, 540 }, 541 } 542 if f.hasMD5 || f.hasSHA1 { 543 opts.Body = bytes.NewBuffer(owncloudProps) 544 } 545 var result api.Multistatus 546 var resp *http.Response 547 err = f.pacer.Call(func() (bool, error) { 548 resp, err = f.srv.CallXML(ctx, &opts, nil, &result) 549 return f.shouldRetry(resp, err) 550 }) 551 if err != nil { 552 if apiErr, ok := err.(*api.Error); ok { 553 // does not exist 554 if apiErr.StatusCode == http.StatusNotFound { 555 if f.retryWithZeroDepth && depth != "0" { 556 return f.listAll(ctx, dir, directoriesOnly, filesOnly, "0", fn) 557 } 558 return found, fs.ErrorDirNotFound 559 } 560 } 561 return found, errors.Wrap(err, "couldn't list files") 562 } 563 //fmt.Printf("result = %#v", &result) 564 baseURL, err := rest.URLJoin(f.endpoint, opts.Path) 565 if err != nil { 566 return false, errors.Wrap(err, "couldn't join URL") 567 } 568 for i := range result.Responses { 569 item := &result.Responses[i] 570 isDir := itemIsDir(item) 571 572 // Find name 573 u, err := rest.URLJoin(baseURL, item.Href) 574 if err != nil { 575 fs.Errorf(nil, "URL Join failed for %q and %q: %v", baseURL, item.Href, err) 576 continue 577 } 578 // Make sure directories end with a / 579 if isDir { 580 u.Path = addSlash(u.Path) 581 } 582 if !strings.HasPrefix(u.Path, baseURL.Path) { 583 fs.Debugf(nil, "Item with unknown path received: %q, %q", u.Path, baseURL.Path) 584 continue 585 } 586 remote := path.Join(dir, u.Path[len(baseURL.Path):]) 587 if strings.HasSuffix(remote, "/") { 588 remote = remote[:len(remote)-1] 589 } 590 591 // the listing contains info about itself which we ignore 592 if remote == dir { 593 continue 594 } 595 596 // Check OK 597 if !item.Props.StatusOK() { 598 fs.Debugf(remote, "Ignoring item with bad status %q", item.Props.Status) 599 continue 600 } 601 602 if isDir { 603 if filesOnly { 604 continue 605 } 606 } else { 607 if directoriesOnly { 608 continue 609 } 610 } 611 // item.Name = restoreReservedChars(item.Name) 612 if fn(remote, isDir, &item.Props) { 613 found = true 614 break 615 } 616 } 617 return 618 } 619 620 // List the objects and directories in dir into entries. The 621 // entries can be returned in any order but should be for a 622 // complete directory. 623 // 624 // dir should be "" to list the root, and should not have 625 // trailing slashes. 626 // 627 // This should return ErrDirNotFound if the directory isn't 628 // found. 629 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 630 var iErr error 631 _, err = f.listAll(ctx, dir, false, false, defaultDepth, func(remote string, isDir bool, info *api.Prop) bool { 632 if isDir { 633 d := fs.NewDir(remote, time.Time(info.Modified)) 634 // .SetID(info.ID) 635 // FIXME more info from dir? can set size, items? 636 entries = append(entries, d) 637 } else { 638 o, err := f.newObjectWithInfo(ctx, remote, info) 639 if err != nil { 640 iErr = err 641 return true 642 } 643 entries = append(entries, o) 644 } 645 return false 646 }) 647 if err != nil { 648 return nil, err 649 } 650 if iErr != nil { 651 return nil, iErr 652 } 653 return entries, nil 654 } 655 656 // Creates from the parameters passed in a half finished Object which 657 // must have setMetaData called on it 658 // 659 // Used to create new objects 660 func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Object) { 661 // Temporary Object under construction 662 o = &Object{ 663 fs: f, 664 remote: remote, 665 size: size, 666 modTime: modTime, 667 } 668 return o 669 } 670 671 // Put the object 672 // 673 // Copy the reader in to the new object which is returned 674 // 675 // The new object may have been created if an error is returned 676 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 677 o := f.createObject(src.Remote(), src.ModTime(ctx), src.Size()) 678 return o, o.Update(ctx, in, src, options...) 679 } 680 681 // PutStream uploads to the remote path with the modTime given of indeterminate size 682 func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 683 return f.Put(ctx, in, src, options...) 684 } 685 686 // mkParentDir makes the parent of the native path dirPath if 687 // necessary and any directories above that 688 func (f *Fs) mkParentDir(ctx context.Context, dirPath string) error { 689 // defer log.Trace(dirPath, "")("") 690 // chop off trailing / if it exists 691 if strings.HasSuffix(dirPath, "/") { 692 dirPath = dirPath[:len(dirPath)-1] 693 } 694 parent := path.Dir(dirPath) 695 if parent == "." { 696 parent = "" 697 } 698 return f.mkdir(ctx, parent) 699 } 700 701 // low level mkdir, only makes the directory, doesn't attempt to create parents 702 func (f *Fs) _mkdir(ctx context.Context, dirPath string) error { 703 // We assume the root is already created 704 if dirPath == "" { 705 return nil 706 } 707 // Collections must end with / 708 if !strings.HasSuffix(dirPath, "/") { 709 dirPath += "/" 710 } 711 opts := rest.Opts{ 712 Method: "MKCOL", 713 Path: dirPath, 714 NoResponse: true, 715 } 716 err := f.pacer.Call(func() (bool, error) { 717 resp, err := f.srv.Call(ctx, &opts) 718 return f.shouldRetry(resp, err) 719 }) 720 if apiErr, ok := err.(*api.Error); ok { 721 // already exists 722 // owncloud returns 423/StatusLocked if the create is already in progress 723 if apiErr.StatusCode == http.StatusMethodNotAllowed || apiErr.StatusCode == http.StatusNotAcceptable || apiErr.StatusCode == http.StatusLocked { 724 return nil 725 } 726 } 727 return err 728 } 729 730 // mkdir makes the directory and parents using native paths 731 func (f *Fs) mkdir(ctx context.Context, dirPath string) error { 732 // defer log.Trace(dirPath, "")("") 733 err := f._mkdir(ctx, dirPath) 734 if apiErr, ok := err.(*api.Error); ok { 735 // parent does not exist so create it first then try again 736 if apiErr.StatusCode == http.StatusConflict { 737 err = f.mkParentDir(ctx, dirPath) 738 if err == nil { 739 err = f._mkdir(ctx, dirPath) 740 } 741 } 742 } 743 return err 744 } 745 746 // Mkdir creates the directory if it doesn't exist 747 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 748 dirPath := f.dirPath(dir) 749 return f.mkdir(ctx, dirPath) 750 } 751 752 // dirNotEmpty returns true if the directory exists and is not Empty 753 // 754 // if the directory does not exist then err will be ErrorDirNotFound 755 func (f *Fs) dirNotEmpty(ctx context.Context, dir string) (found bool, err error) { 756 return f.listAll(ctx, dir, false, false, defaultDepth, func(remote string, isDir bool, info *api.Prop) bool { 757 return true 758 }) 759 } 760 761 // purgeCheck removes the root directory, if check is set then it 762 // refuses to do so if it has anything in 763 func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error { 764 if check { 765 notEmpty, err := f.dirNotEmpty(ctx, dir) 766 if err != nil { 767 return err 768 } 769 if notEmpty { 770 return fs.ErrorDirectoryNotEmpty 771 } 772 } 773 opts := rest.Opts{ 774 Method: "DELETE", 775 Path: f.dirPath(dir), 776 NoResponse: true, 777 } 778 var resp *http.Response 779 var err error 780 err = f.pacer.Call(func() (bool, error) { 781 resp, err = f.srv.CallXML(ctx, &opts, nil, nil) 782 return f.shouldRetry(resp, err) 783 }) 784 if err != nil { 785 return errors.Wrap(err, "rmdir failed") 786 } 787 // FIXME parse Multistatus response 788 return nil 789 } 790 791 // Rmdir deletes the root folder 792 // 793 // Returns an error if it isn't empty 794 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 795 return f.purgeCheck(ctx, dir, true) 796 } 797 798 // Precision return the precision of this Fs 799 func (f *Fs) Precision() time.Duration { 800 return f.precision 801 } 802 803 // Copy or Move src to this remote using server side copy operations. 804 // 805 // This is stored with the remote path given 806 // 807 // It returns the destination Object and a possible error 808 // 809 // Will only be called if src.Fs().Name() == f.Name() 810 // 811 // If it isn't possible then return fs.ErrorCantCopy/fs.ErrorCantMove 812 func (f *Fs) copyOrMove(ctx context.Context, src fs.Object, remote string, method string) (fs.Object, error) { 813 srcObj, ok := src.(*Object) 814 if !ok { 815 fs.Debugf(src, "Can't copy - not same remote type") 816 if method == "COPY" { 817 return nil, fs.ErrorCantCopy 818 } 819 return nil, fs.ErrorCantMove 820 } 821 dstPath := f.filePath(remote) 822 err := f.mkParentDir(ctx, dstPath) 823 if err != nil { 824 return nil, errors.Wrap(err, "Copy mkParentDir failed") 825 } 826 destinationURL, err := rest.URLJoin(f.endpoint, dstPath) 827 if err != nil { 828 return nil, errors.Wrap(err, "copyOrMove couldn't join URL") 829 } 830 var resp *http.Response 831 opts := rest.Opts{ 832 Method: method, 833 Path: srcObj.filePath(), 834 NoResponse: true, 835 ExtraHeaders: map[string]string{ 836 "Destination": destinationURL.String(), 837 "Overwrite": "F", 838 }, 839 } 840 if f.useOCMtime { 841 opts.ExtraHeaders["X-OC-Mtime"] = fmt.Sprintf("%f", float64(src.ModTime(ctx).UnixNano())/1e9) 842 } 843 err = f.pacer.Call(func() (bool, error) { 844 resp, err = f.srv.Call(ctx, &opts) 845 return f.shouldRetry(resp, err) 846 }) 847 if err != nil { 848 return nil, errors.Wrap(err, "Copy call failed") 849 } 850 dstObj, err := f.NewObject(ctx, remote) 851 if err != nil { 852 return nil, errors.Wrap(err, "Copy NewObject failed") 853 } 854 return dstObj, nil 855 } 856 857 // Copy src to this remote using server side copy operations. 858 // 859 // This is stored with the remote path given 860 // 861 // It returns the destination Object and a possible error 862 // 863 // Will only be called if src.Fs().Name() == f.Name() 864 // 865 // If it isn't possible then return fs.ErrorCantCopy 866 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 867 return f.copyOrMove(ctx, src, remote, "COPY") 868 } 869 870 // Purge deletes all the files and the container 871 // 872 // Optional interface: Only implement this if you have a way of 873 // deleting all the files quicker than just running Remove() on the 874 // result of List() 875 func (f *Fs) Purge(ctx context.Context) error { 876 return f.purgeCheck(ctx, "", false) 877 } 878 879 // Move src to this remote using server side move operations. 880 // 881 // This is stored with the remote path given 882 // 883 // It returns the destination Object and a possible error 884 // 885 // Will only be called if src.Fs().Name() == f.Name() 886 // 887 // If it isn't possible then return fs.ErrorCantMove 888 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 889 return f.copyOrMove(ctx, src, remote, "MOVE") 890 } 891 892 // DirMove moves src, srcRemote to this remote at dstRemote 893 // using server side move operations. 894 // 895 // Will only be called if src.Fs().Name() == f.Name() 896 // 897 // If it isn't possible then return fs.ErrorCantDirMove 898 // 899 // If destination exists then return fs.ErrorDirExists 900 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error { 901 srcFs, ok := src.(*Fs) 902 if !ok { 903 fs.Debugf(srcFs, "Can't move directory - not same remote type") 904 return fs.ErrorCantDirMove 905 } 906 srcPath := srcFs.filePath(srcRemote) 907 dstPath := f.filePath(dstRemote) 908 909 // Check if destination exists 910 _, err := f.dirNotEmpty(ctx, dstRemote) 911 if err == nil { 912 return fs.ErrorDirExists 913 } 914 if err != fs.ErrorDirNotFound { 915 return errors.Wrap(err, "DirMove dirExists dst failed") 916 } 917 918 // Make sure the parent directory exists 919 err = f.mkParentDir(ctx, dstPath) 920 if err != nil { 921 return errors.Wrap(err, "DirMove mkParentDir dst failed") 922 } 923 924 destinationURL, err := rest.URLJoin(f.endpoint, dstPath) 925 if err != nil { 926 return errors.Wrap(err, "DirMove couldn't join URL") 927 } 928 929 var resp *http.Response 930 opts := rest.Opts{ 931 Method: "MOVE", 932 Path: addSlash(srcPath), 933 NoResponse: true, 934 ExtraHeaders: map[string]string{ 935 "Destination": addSlash(destinationURL.String()), 936 "Overwrite": "F", 937 }, 938 } 939 err = f.pacer.Call(func() (bool, error) { 940 resp, err = f.srv.Call(ctx, &opts) 941 return f.shouldRetry(resp, err) 942 }) 943 if err != nil { 944 return errors.Wrap(err, "DirMove MOVE call failed") 945 } 946 return nil 947 } 948 949 // Hashes returns the supported hash sets. 950 func (f *Fs) Hashes() hash.Set { 951 hashes := hash.Set(hash.None) 952 if f.hasMD5 { 953 hashes.Add(hash.MD5) 954 } 955 if f.hasSHA1 { 956 hashes.Add(hash.SHA1) 957 } 958 return hashes 959 } 960 961 // About gets quota information 962 func (f *Fs) About(ctx context.Context) (*fs.Usage, error) { 963 opts := rest.Opts{ 964 Method: "PROPFIND", 965 Path: "", 966 ExtraHeaders: map[string]string{ 967 "Depth": "0", 968 }, 969 } 970 opts.Body = bytes.NewBuffer([]byte(`<?xml version="1.0" ?> 971 <D:propfind xmlns:D="DAV:"> 972 <D:prop> 973 <D:quota-available-bytes/> 974 <D:quota-used-bytes/> 975 </D:prop> 976 </D:propfind> 977 `)) 978 var q = api.Quota{ 979 Available: -1, 980 Used: -1, 981 } 982 var resp *http.Response 983 var err error 984 err = f.pacer.Call(func() (bool, error) { 985 resp, err = f.srv.CallXML(ctx, &opts, nil, &q) 986 return f.shouldRetry(resp, err) 987 }) 988 if err != nil { 989 return nil, errors.Wrap(err, "about call failed") 990 } 991 usage := &fs.Usage{} 992 if q.Available != 0 || q.Used != 0 { 993 if q.Available >= 0 && q.Used >= 0 { 994 usage.Total = fs.NewUsageValue(q.Available + q.Used) 995 } 996 if q.Used >= 0 { 997 usage.Used = fs.NewUsageValue(q.Used) 998 } 999 } 1000 return usage, nil 1001 } 1002 1003 // ------------------------------------------------------------ 1004 1005 // Fs returns the parent Fs 1006 func (o *Object) Fs() fs.Info { 1007 return o.fs 1008 } 1009 1010 // Return a string version 1011 func (o *Object) String() string { 1012 if o == nil { 1013 return "<nil>" 1014 } 1015 return o.remote 1016 } 1017 1018 // Remote returns the remote path 1019 func (o *Object) Remote() string { 1020 return o.remote 1021 } 1022 1023 // Hash returns the SHA1 or MD5 of an object returning a lowercase hex string 1024 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 1025 if t == hash.MD5 && o.fs.hasMD5 { 1026 return o.md5, nil 1027 } 1028 if t == hash.SHA1 && o.fs.hasSHA1 { 1029 return o.sha1, nil 1030 } 1031 return "", hash.ErrUnsupported 1032 } 1033 1034 // Size returns the size of an object in bytes 1035 func (o *Object) Size() int64 { 1036 ctx := context.TODO() 1037 err := o.readMetaData(ctx) 1038 if err != nil { 1039 fs.Logf(o, "Failed to read metadata: %v", err) 1040 return 0 1041 } 1042 return o.size 1043 } 1044 1045 // setMetaData sets the metadata from info 1046 func (o *Object) setMetaData(info *api.Prop) (err error) { 1047 o.hasMetaData = true 1048 o.size = info.Size 1049 o.modTime = time.Time(info.Modified) 1050 if o.fs.hasMD5 || o.fs.hasSHA1 { 1051 hashes := info.Hashes() 1052 if o.fs.hasSHA1 { 1053 o.sha1 = hashes[hash.SHA1] 1054 } 1055 if o.fs.hasMD5 { 1056 o.md5 = hashes[hash.MD5] 1057 } 1058 } 1059 return nil 1060 } 1061 1062 // readMetaData gets the metadata if it hasn't already been fetched 1063 // 1064 // it also sets the info 1065 func (o *Object) readMetaData(ctx context.Context) (err error) { 1066 if o.hasMetaData { 1067 return nil 1068 } 1069 info, err := o.fs.readMetaDataForPath(ctx, o.remote, defaultDepth) 1070 if err != nil { 1071 return err 1072 } 1073 return o.setMetaData(info) 1074 } 1075 1076 // ModTime returns the modification time of the object 1077 // 1078 // It attempts to read the objects mtime and if that isn't present the 1079 // LastModified returned in the http headers 1080 func (o *Object) ModTime(ctx context.Context) time.Time { 1081 err := o.readMetaData(ctx) 1082 if err != nil { 1083 fs.Logf(o, "Failed to read metadata: %v", err) 1084 return time.Now() 1085 } 1086 return o.modTime 1087 } 1088 1089 // SetModTime sets the modification time of the local fs object 1090 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 1091 return fs.ErrorCantSetModTime 1092 } 1093 1094 // Storable returns a boolean showing whether this object storable 1095 func (o *Object) Storable() bool { 1096 return true 1097 } 1098 1099 // Open an object for read 1100 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 1101 var resp *http.Response 1102 opts := rest.Opts{ 1103 Method: "GET", 1104 Path: o.filePath(), 1105 Options: options, 1106 } 1107 err = o.fs.pacer.Call(func() (bool, error) { 1108 resp, err = o.fs.srv.Call(ctx, &opts) 1109 return o.fs.shouldRetry(resp, err) 1110 }) 1111 if err != nil { 1112 return nil, err 1113 } 1114 return resp.Body, err 1115 } 1116 1117 // Update the object with the contents of the io.Reader, modTime and size 1118 // 1119 // If existing is set then it updates the object rather than creating a new one 1120 // 1121 // The new object may have been created if an error is returned 1122 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 1123 err = o.fs.mkParentDir(ctx, o.filePath()) 1124 if err != nil { 1125 return errors.Wrap(err, "Update mkParentDir failed") 1126 } 1127 1128 size := src.Size() 1129 var resp *http.Response 1130 opts := rest.Opts{ 1131 Method: "PUT", 1132 Path: o.filePath(), 1133 Body: in, 1134 NoResponse: true, 1135 ContentLength: &size, // FIXME this isn't necessary with owncloud - See https://github.com/nextcloud/nextcloud-snap/issues/365 1136 ContentType: fs.MimeType(ctx, src), 1137 } 1138 if o.fs.useOCMtime || o.fs.hasMD5 || o.fs.hasSHA1 { 1139 opts.ExtraHeaders = map[string]string{} 1140 if o.fs.useOCMtime { 1141 opts.ExtraHeaders["X-OC-Mtime"] = fmt.Sprintf("%f", float64(src.ModTime(ctx).UnixNano())/1e9) 1142 } 1143 // Set one upload checksum 1144 // Owncloud uses one checksum only to check the upload and stores its own SHA1 and MD5 1145 // Nextcloud stores the checksum you supply (SHA1 or MD5) but only stores one 1146 if o.fs.hasSHA1 { 1147 if sha1, _ := src.Hash(ctx, hash.SHA1); sha1 != "" { 1148 opts.ExtraHeaders["OC-Checksum"] = "SHA1:" + sha1 1149 } 1150 } 1151 if o.fs.hasMD5 && opts.ExtraHeaders["OC-Checksum"] == "" { 1152 if md5, _ := src.Hash(ctx, hash.MD5); md5 != "" { 1153 opts.ExtraHeaders["OC-Checksum"] = "MD5:" + md5 1154 } 1155 } 1156 } 1157 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 1158 resp, err = o.fs.srv.Call(ctx, &opts) 1159 return o.fs.shouldRetry(resp, err) 1160 }) 1161 if err != nil { 1162 // Give the WebDAV server a chance to get its internal state in order after the 1163 // error. The error may have been local in which case we closed the connection. 1164 // The server may still be dealing with it for a moment. A sleep isn't ideal but I 1165 // haven't been able to think of a better method to find out if the server has 1166 // finished - ncw 1167 time.Sleep(1 * time.Second) 1168 // Remove failed upload 1169 _ = o.Remove(ctx) 1170 return err 1171 } 1172 // read metadata from remote 1173 o.hasMetaData = false 1174 return o.readMetaData(ctx) 1175 } 1176 1177 // Remove an object 1178 func (o *Object) Remove(ctx context.Context) error { 1179 opts := rest.Opts{ 1180 Method: "DELETE", 1181 Path: o.filePath(), 1182 NoResponse: true, 1183 } 1184 return o.fs.pacer.Call(func() (bool, error) { 1185 resp, err := o.fs.srv.Call(ctx, &opts) 1186 return o.fs.shouldRetry(resp, err) 1187 }) 1188 } 1189 1190 // Check the interfaces are satisfied 1191 var ( 1192 _ fs.Fs = (*Fs)(nil) 1193 _ fs.Purger = (*Fs)(nil) 1194 _ fs.PutStreamer = (*Fs)(nil) 1195 _ fs.Copier = (*Fs)(nil) 1196 _ fs.Mover = (*Fs)(nil) 1197 _ fs.DirMover = (*Fs)(nil) 1198 _ fs.Abouter = (*Fs)(nil) 1199 _ fs.Object = (*Object)(nil) 1200 )