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