github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/opendrive/opendrive.go (about) 1 // Package opendrive provides an interface to the OpenDrive storage system. 2 package opendrive 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "path" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/rclone/rclone/fs" 17 "github.com/rclone/rclone/fs/config" 18 "github.com/rclone/rclone/fs/config/configmap" 19 "github.com/rclone/rclone/fs/config/configstruct" 20 "github.com/rclone/rclone/fs/config/obscure" 21 "github.com/rclone/rclone/fs/fserrors" 22 "github.com/rclone/rclone/fs/fshttp" 23 "github.com/rclone/rclone/fs/hash" 24 "github.com/rclone/rclone/lib/dircache" 25 "github.com/rclone/rclone/lib/encoder" 26 "github.com/rclone/rclone/lib/pacer" 27 "github.com/rclone/rclone/lib/readers" 28 "github.com/rclone/rclone/lib/rest" 29 ) 30 31 const ( 32 defaultEndpoint = "https://dev.opendrive.com/api/v1" 33 minSleep = 10 * time.Millisecond 34 maxSleep = 5 * time.Minute 35 decayConstant = 1 // bigger for slower decay, exponential 36 ) 37 38 // Register with Fs 39 func init() { 40 fs.Register(&fs.RegInfo{ 41 Name: "opendrive", 42 Description: "OpenDrive", 43 NewFs: NewFs, 44 Options: []fs.Option{{ 45 Name: "username", 46 Help: "Username.", 47 Required: true, 48 Sensitive: true, 49 }, { 50 Name: "password", 51 Help: "Password.", 52 IsPassword: true, 53 Required: true, 54 }, { 55 Name: config.ConfigEncoding, 56 Help: config.ConfigEncodingHelp, 57 Advanced: true, 58 // List of replaced characters: 59 // < (less than) -> '<' // FULLWIDTH LESS-THAN SIGN 60 // > (greater than) -> '>' // FULLWIDTH GREATER-THAN SIGN 61 // : (colon) -> ':' // FULLWIDTH COLON 62 // " (double quote) -> '"' // FULLWIDTH QUOTATION MARK 63 // \ (backslash) -> '\' // FULLWIDTH REVERSE SOLIDUS 64 // | (vertical line) -> '|' // FULLWIDTH VERTICAL LINE 65 // ? (question mark) -> '?' // FULLWIDTH QUESTION MARK 66 // * (asterisk) -> '*' // FULLWIDTH ASTERISK 67 // 68 // Additionally names can't begin or end with an ASCII whitespace. 69 // List of replaced characters: 70 // (space) -> '␠' // SYMBOL FOR SPACE 71 // (horizontal tab) -> '␉' // SYMBOL FOR HORIZONTAL TABULATION 72 // (line feed) -> '␊' // SYMBOL FOR LINE FEED 73 // (vertical tab) -> '␋' // SYMBOL FOR VERTICAL TABULATION 74 // (carriage return) -> '␍' // SYMBOL FOR CARRIAGE RETURN 75 // 76 // Also encode invalid UTF-8 bytes as json doesn't handle them properly. 77 // 78 // https://www.opendrive.com/wp-content/uploads/guides/OpenDrive_API_guide.pdf 79 Default: (encoder.Base | 80 encoder.EncodeWin | 81 encoder.EncodeLeftCrLfHtVt | 82 encoder.EncodeRightCrLfHtVt | 83 encoder.EncodeBackSlash | 84 encoder.EncodeLeftSpace | 85 encoder.EncodeRightSpace | 86 encoder.EncodeInvalidUtf8), 87 }, { 88 Name: "chunk_size", 89 Help: `Files will be uploaded in chunks this size. 90 91 Note that these chunks are buffered in memory so increasing them will 92 increase memory use.`, 93 Default: 10 * fs.Mebi, 94 Advanced: true, 95 }}, 96 }) 97 } 98 99 // Options defines the configuration for this backend 100 type Options struct { 101 UserName string `config:"username"` 102 Password string `config:"password"` 103 Enc encoder.MultiEncoder `config:"encoding"` 104 ChunkSize fs.SizeSuffix `config:"chunk_size"` 105 } 106 107 // Fs represents a remote server 108 type Fs struct { 109 name string // name of this remote 110 root string // the path we are working on 111 opt Options // parsed options 112 features *fs.Features // optional features 113 srv *rest.Client // the connection to the server 114 pacer *fs.Pacer // To pace and retry the API calls 115 session UserSessionInfo // contains the session data 116 dirCache *dircache.DirCache // Map of directory path to directory id 117 } 118 119 // Object describes an object 120 type Object struct { 121 fs *Fs // what this object is part of 122 remote string // The remote path 123 id string // ID of the file 124 parent string // ID of the parent directory 125 modTime time.Time // The modified time of the object if known 126 md5 string // MD5 hash if known 127 size int64 // Size of the object 128 } 129 130 // parsePath parses an incoming 'url' 131 func parsePath(path string) (root string) { 132 root = strings.Trim(path, "/") 133 return 134 } 135 136 // ------------------------------------------------------------ 137 138 // Name of the remote (as passed into NewFs) 139 func (f *Fs) Name() string { 140 return f.name 141 } 142 143 // Root of the remote (as passed into NewFs) 144 func (f *Fs) Root() string { 145 return f.root 146 } 147 148 // String converts this Fs to a string 149 func (f *Fs) String() string { 150 return fmt.Sprintf("OpenDrive root '%s'", f.root) 151 } 152 153 // Features returns the optional features of this Fs 154 func (f *Fs) Features() *fs.Features { 155 return f.features 156 } 157 158 // Hashes returns the supported hash sets. 159 func (f *Fs) Hashes() hash.Set { 160 return hash.Set(hash.MD5) 161 } 162 163 // DirCacheFlush resets the directory cache - used in testing as an 164 // optional interface 165 func (f *Fs) DirCacheFlush() { 166 f.dirCache.ResetRoot() 167 } 168 169 // NewFs constructs an Fs from the path, bucket:path 170 func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) { 171 // Parse config into Options struct 172 opt := new(Options) 173 err := configstruct.Set(m, opt) 174 if err != nil { 175 return nil, err 176 } 177 root = parsePath(root) 178 if opt.UserName == "" { 179 return nil, errors.New("username not found") 180 } 181 opt.Password, err = obscure.Reveal(opt.Password) 182 if err != nil { 183 return nil, errors.New("password could not revealed") 184 } 185 if opt.Password == "" { 186 return nil, errors.New("password not found") 187 } 188 189 f := &Fs{ 190 name: name, 191 root: root, 192 opt: *opt, 193 srv: rest.NewClient(fshttp.NewClient(ctx)).SetErrorHandler(errorHandler), 194 pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 195 } 196 197 f.dirCache = dircache.New(root, "0", f) 198 199 // set the rootURL for the REST client 200 f.srv.SetRoot(defaultEndpoint) 201 202 // get sessionID 203 var resp *http.Response 204 err = f.pacer.Call(func() (bool, error) { 205 account := Account{Username: opt.UserName, Password: opt.Password} 206 207 opts := rest.Opts{ 208 Method: "POST", 209 Path: "/session/login.json", 210 } 211 resp, err = f.srv.CallJSON(ctx, &opts, &account, &f.session) 212 return f.shouldRetry(ctx, resp, err) 213 }) 214 if err != nil { 215 return nil, fmt.Errorf("failed to create session: %w", err) 216 } 217 fs.Debugf(nil, "Starting OpenDrive session with ID: %s", f.session.SessionID) 218 219 f.features = (&fs.Features{ 220 CaseInsensitive: true, 221 CanHaveEmptyDirectories: true, 222 }).Fill(ctx, f) 223 224 // Find the current root 225 err = f.dirCache.FindRoot(ctx, false) 226 if err != nil { 227 // Assume it is a file 228 newRoot, remote := dircache.SplitPath(root) 229 tempF := *f 230 tempF.dirCache = dircache.New(newRoot, "0", &tempF) 231 tempF.root = newRoot 232 233 // Make new Fs which is the parent 234 err = tempF.dirCache.FindRoot(ctx, false) 235 if err != nil { 236 // No root so return old f 237 return f, nil 238 } 239 _, err := tempF.newObjectWithInfo(ctx, remote, nil, "") 240 if err != nil { 241 if err == fs.ErrorObjectNotFound { 242 // File doesn't exist so return old f 243 return f, nil 244 } 245 return nil, err 246 } 247 // XXX: update the old f here instead of returning tempF, since 248 // `features` were already filled with functions having *f as a receiver. 249 // See https://github.com/rclone/rclone/issues/2182 250 f.dirCache = tempF.dirCache 251 f.root = tempF.root 252 // return an error with an fs which points to the parent 253 return f, fs.ErrorIsFile 254 } 255 return f, nil 256 } 257 258 // rootSlash returns root with a slash on if it is empty, otherwise empty string 259 func (f *Fs) rootSlash() string { 260 if f.root == "" { 261 return f.root 262 } 263 return f.root + "/" 264 } 265 266 // errorHandler parses a non 2xx error response into an error 267 func errorHandler(resp *http.Response) error { 268 errResponse := new(Error) 269 err := rest.DecodeJSON(resp, &errResponse) 270 if err != nil { 271 fs.Debugf(nil, "Couldn't decode error response: %v", err) 272 } 273 if errResponse.Info.Code == 0 { 274 errResponse.Info.Code = resp.StatusCode 275 } 276 if errResponse.Info.Message == "" { 277 errResponse.Info.Message = "Unknown " + resp.Status 278 } 279 return errResponse 280 } 281 282 // Mkdir creates the folder if it doesn't exist 283 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 284 // fs.Debugf(nil, "Mkdir(\"%s\")", dir) 285 _, err := f.dirCache.FindDir(ctx, dir, true) 286 return err 287 } 288 289 // deleteObject removes an object by ID 290 func (f *Fs) deleteObject(ctx context.Context, id string) error { 291 return f.pacer.Call(func() (bool, error) { 292 removeDirData := removeFolder{SessionID: f.session.SessionID, FolderID: id} 293 opts := rest.Opts{ 294 Method: "POST", 295 NoResponse: true, 296 Path: "/folder/remove.json", 297 } 298 resp, err := f.srv.CallJSON(ctx, &opts, &removeDirData, nil) 299 return f.shouldRetry(ctx, resp, err) 300 }) 301 } 302 303 // purgeCheck remotes the root directory, if check is set then it 304 // refuses to do so if it has anything in 305 func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error { 306 root := path.Join(f.root, dir) 307 if root == "" { 308 return errors.New("can't purge root directory") 309 } 310 dc := f.dirCache 311 rootID, err := dc.FindDir(ctx, dir, false) 312 if err != nil { 313 return err 314 } 315 item, err := f.readMetaDataForFolderID(ctx, rootID) 316 if err != nil { 317 return err 318 } 319 if check && len(item.Files) != 0 { 320 return errors.New("folder not empty") 321 } 322 err = f.deleteObject(ctx, rootID) 323 if err != nil { 324 return err 325 } 326 f.dirCache.FlushDir(dir) 327 return nil 328 } 329 330 // Rmdir deletes the root folder 331 // 332 // Returns an error if it isn't empty 333 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 334 // fs.Debugf(nil, "Rmdir(\"%s\")", path.Join(f.root, dir)) 335 return f.purgeCheck(ctx, dir, true) 336 } 337 338 // Precision of the remote 339 func (f *Fs) Precision() time.Duration { 340 return time.Second 341 } 342 343 // Copy src to this remote using server-side copy operations. 344 // 345 // This is stored with the remote path given. 346 // 347 // It returns the destination Object and a possible error. 348 // 349 // Will only be called if src.Fs().Name() == f.Name() 350 // 351 // If it isn't possible then return fs.ErrorCantCopy 352 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 353 // fs.Debugf(nil, "Copy(%v)", remote) 354 srcObj, ok := src.(*Object) 355 if !ok { 356 fs.Debugf(src, "Can't copy - not same remote type") 357 return nil, fs.ErrorCantCopy 358 } 359 err := srcObj.readMetaData(ctx) 360 if err != nil { 361 return nil, err 362 } 363 364 srcPath := srcObj.fs.rootSlash() + srcObj.remote 365 dstPath := f.rootSlash() + remote 366 if strings.EqualFold(srcPath, dstPath) { 367 return nil, fmt.Errorf("can't copy %q -> %q as are same name when lowercase", srcPath, dstPath) 368 } 369 370 // Create temporary object 371 dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size) 372 if err != nil { 373 return nil, err 374 } 375 // fs.Debugf(nil, "...%#v\n...%#v", remote, directoryID) 376 377 // Copy the object 378 var resp *http.Response 379 response := moveCopyFileResponse{} 380 err = f.pacer.Call(func() (bool, error) { 381 copyFileData := moveCopyFile{ 382 SessionID: f.session.SessionID, 383 SrcFileID: srcObj.id, 384 DstFolderID: directoryID, 385 Move: "false", 386 OverwriteIfExists: "true", 387 NewFileName: leaf, 388 } 389 opts := rest.Opts{ 390 Method: "POST", 391 Path: "/file/move_copy.json", 392 } 393 resp, err = f.srv.CallJSON(ctx, &opts, ©FileData, &response) 394 return f.shouldRetry(ctx, resp, err) 395 }) 396 if err != nil { 397 return nil, err 398 } 399 400 size, _ := strconv.ParseInt(response.Size, 10, 64) 401 dstObj.id = response.FileID 402 dstObj.size = size 403 404 return dstObj, nil 405 } 406 407 // Move src to this remote using server-side move operations. 408 // 409 // This is stored with the remote path given. 410 // 411 // It returns the destination Object and a possible error. 412 // 413 // Will only be called if src.Fs().Name() == f.Name() 414 // 415 // If it isn't possible then return fs.ErrorCantMove 416 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 417 // fs.Debugf(nil, "Move(%v)", remote) 418 srcObj, ok := src.(*Object) 419 if !ok { 420 fs.Debugf(src, "Can't move - not same remote type") 421 return nil, fs.ErrorCantCopy 422 } 423 err := srcObj.readMetaData(ctx) 424 if err != nil { 425 return nil, err 426 } 427 428 // Create temporary object 429 dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size) 430 if err != nil { 431 return nil, err 432 } 433 434 // move_copy will silently truncate new filenames 435 if len(leaf) > 255 { 436 fs.Debugf(src, "Can't move file: name (%q) exceeds 255 char", leaf) 437 return nil, fs.ErrorFileNameTooLong 438 } 439 440 moveCopyFileData := moveCopyFile{ 441 SessionID: f.session.SessionID, 442 SrcFileID: srcObj.id, 443 DstFolderID: directoryID, 444 Move: "true", 445 OverwriteIfExists: "true", 446 NewFileName: leaf, 447 } 448 opts := rest.Opts{ 449 Method: "POST", 450 Path: "/file/move_copy.json", 451 } 452 var request interface{} = moveCopyFileData 453 454 // use /file/rename.json if moving within the same directory 455 _, srcDirID, err := srcObj.fs.dirCache.FindPath(ctx, srcObj.remote, false) 456 if err != nil { 457 return nil, err 458 } 459 if srcDirID == directoryID { 460 fs.Debugf(src, "same parent dir (%v) - using file/rename instead of move_copy for %s", directoryID, remote) 461 renameFileData := renameFile{ 462 SessionID: f.session.SessionID, 463 FileID: srcObj.id, 464 NewFileName: leaf, 465 } 466 opts.Path = "/file/rename.json" 467 request = renameFileData 468 } 469 470 // Move the object 471 var resp *http.Response 472 response := moveCopyFileResponse{} 473 err = f.pacer.Call(func() (bool, error) { 474 resp, err = f.srv.CallJSON(ctx, &opts, &request, &response) 475 return f.shouldRetry(ctx, resp, err) 476 }) 477 if err != nil { 478 return nil, err 479 } 480 481 size, _ := strconv.ParseInt(response.Size, 10, 64) 482 dstObj.id = response.FileID 483 dstObj.size = size 484 485 return dstObj, nil 486 } 487 488 // DirMove moves src, srcRemote to this remote at dstRemote 489 // using server-side move operations. 490 // 491 // Will only be called if src.Fs().Name() == f.Name() 492 // 493 // If it isn't possible then return fs.ErrorCantDirMove 494 // 495 // If destination exists then return fs.ErrorDirExists 496 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) (err error) { 497 srcFs, ok := src.(*Fs) 498 if !ok { 499 fs.Debugf(srcFs, "Can't move directory - not same remote type") 500 return fs.ErrorCantDirMove 501 } 502 503 srcID, srcDirectoryID, _, dstDirectoryID, dstLeaf, err := f.dirCache.DirMove(ctx, srcFs.dirCache, srcFs.root, srcRemote, f.root, dstRemote) 504 if err != nil { 505 return err 506 } 507 508 // move_copy will silently truncate new filenames 509 if len(dstLeaf) > 255 { 510 fs.Debugf(src, "Can't move folder: name (%q) exceeds 255 char", dstLeaf) 511 return fs.ErrorFileNameTooLong 512 } 513 514 moveFolderData := moveCopyFolder{ 515 SessionID: f.session.SessionID, 516 FolderID: srcID, 517 DstFolderID: dstDirectoryID, 518 Move: "true", 519 NewFolderName: dstLeaf, 520 } 521 opts := rest.Opts{ 522 Method: "POST", 523 Path: "/folder/move_copy.json", 524 } 525 var request interface{} = moveFolderData 526 527 // use /folder/rename.json if moving within the same parent directory 528 if srcDirectoryID == dstDirectoryID { 529 fs.Debugf(dstRemote, "same parent dir (%v) - using folder/rename instead of move_copy", srcDirectoryID) 530 renameFolderData := renameFolder{ 531 SessionID: f.session.SessionID, 532 FolderID: srcID, 533 FolderName: dstLeaf, 534 } 535 opts.Path = "/folder/rename.json" 536 request = renameFolderData 537 } 538 539 // Do the move 540 var resp *http.Response 541 response := moveCopyFolderResponse{} 542 err = f.pacer.Call(func() (bool, error) { 543 resp, err = f.srv.CallJSON(ctx, &opts, &request, &response) 544 return f.shouldRetry(ctx, resp, err) 545 }) 546 if err != nil { 547 fs.Debugf(src, "DirMove error %v", err) 548 return err 549 } 550 551 srcFs.dirCache.FlushDir(srcRemote) 552 return nil 553 } 554 555 // Purge deletes all the files in the directory 556 // 557 // Optional interface: Only implement this if you have a way of 558 // deleting all the files quicker than just running Remove() on the 559 // result of List() 560 func (f *Fs) Purge(ctx context.Context, dir string) error { 561 return f.purgeCheck(ctx, dir, false) 562 } 563 564 // Return an Object from a path 565 // 566 // If it can't be found it returns the error fs.ErrorObjectNotFound. 567 func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, file *File, parent string) (fs.Object, error) { 568 // fs.Debugf(nil, "newObjectWithInfo(%s, %v)", remote, file) 569 570 var o *Object 571 if nil != file { 572 o = &Object{ 573 fs: f, 574 remote: remote, 575 id: file.FileID, 576 parent: parent, 577 modTime: time.Unix(file.DateModified, 0), 578 size: file.Size, 579 md5: file.FileHash, 580 } 581 } else { 582 o = &Object{ 583 fs: f, 584 remote: remote, 585 } 586 587 err := o.readMetaData(ctx) 588 if err != nil { 589 return nil, err 590 } 591 } 592 return o, nil 593 } 594 595 // NewObject finds the Object at remote. If it can't be found 596 // it returns the error fs.ErrorObjectNotFound. 597 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 598 // fs.Debugf(nil, "NewObject(\"%s\")", remote) 599 return f.newObjectWithInfo(ctx, remote, nil, "") 600 } 601 602 // Creates from the parameters passed in a half finished Object which 603 // must have setMetaData called on it 604 // 605 // Returns the object, leaf, directoryID and error. 606 // 607 // Used to create new objects 608 func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, leaf string, directoryID string, err error) { 609 // Create the directory for the object if it doesn't exist 610 leaf, directoryID, err = f.dirCache.FindPath(ctx, remote, true) 611 if err != nil { 612 return nil, leaf, directoryID, err 613 } 614 // fs.Debugf(nil, "\n...leaf %#v\n...id %#v", leaf, directoryID) 615 // Temporary Object under construction 616 o = &Object{ 617 fs: f, 618 remote: remote, 619 } 620 return o, f.opt.Enc.FromStandardName(leaf), directoryID, nil 621 } 622 623 // readMetaDataForPath reads the metadata from the path 624 func (f *Fs) readMetaDataForFolderID(ctx context.Context, id string) (info *FolderList, err error) { 625 var resp *http.Response 626 opts := rest.Opts{ 627 Method: "GET", 628 Path: "/folder/list.json/" + f.session.SessionID + "/" + id, 629 } 630 err = f.pacer.Call(func() (bool, error) { 631 resp, err = f.srv.CallJSON(ctx, &opts, nil, &info) 632 return f.shouldRetry(ctx, resp, err) 633 }) 634 if err != nil { 635 return nil, err 636 } 637 return info, err 638 } 639 640 // Put the object into the bucket 641 // 642 // Copy the reader in to the new object which is returned. 643 // 644 // The new object may have been created if an error is returned 645 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 646 remote := src.Remote() 647 size := src.Size() 648 modTime := src.ModTime(ctx) 649 650 // fs.Debugf(nil, "Put(%s)", remote) 651 652 o, leaf, directoryID, err := f.createObject(ctx, remote, modTime, size) 653 if err != nil { 654 return nil, err 655 } 656 657 if o.id == "" { 658 // Attempt to read ID, ignore error 659 // FIXME is this correct? 660 _ = o.readMetaData(ctx) 661 } 662 663 if o.id == "" { 664 // We need to create an ID for this file 665 var resp *http.Response 666 response := createFileResponse{} 667 err := o.fs.pacer.Call(func() (bool, error) { 668 createFileData := createFile{ 669 SessionID: o.fs.session.SessionID, 670 FolderID: directoryID, 671 Name: leaf, 672 } 673 opts := rest.Opts{ 674 Method: "POST", 675 Options: options, 676 Path: "/upload/create_file.json", 677 } 678 resp, err = o.fs.srv.CallJSON(ctx, &opts, &createFileData, &response) 679 return o.fs.shouldRetry(ctx, resp, err) 680 }) 681 if err != nil { 682 return nil, fmt.Errorf("failed to create file: %w", err) 683 } 684 685 o.id = response.FileID 686 } 687 688 return o, o.Update(ctx, in, src, options...) 689 } 690 691 // retryErrorCodes is a slice of error codes that we will retry 692 var retryErrorCodes = []int{ 693 401, // Unauthorized (seen in "Token has expired") 694 408, // Request Timeout 695 423, // Locked - get this on folders sometimes 696 429, // Rate exceeded. 697 500, // Get occasional 500 Internal Server Error 698 502, // Bad Gateway when doing big listings 699 503, // Service Unavailable 700 504, // Gateway Time-out 701 } 702 703 // shouldRetry returns a boolean as to whether this resp and err 704 // deserve to be retried. It returns the err as a convenience 705 func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) { 706 if fserrors.ContextError(ctx, &err) { 707 return false, err 708 } 709 return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 710 } 711 712 // DirCacher methods 713 714 // CreateDir makes a directory with pathID as parent and name leaf 715 func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) { 716 // fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, replaceReservedChars(leaf)) 717 var resp *http.Response 718 response := createFolderResponse{} 719 err = f.pacer.Call(func() (bool, error) { 720 createDirData := createFolder{ 721 SessionID: f.session.SessionID, 722 FolderName: f.opt.Enc.FromStandardName(leaf), 723 FolderSubParent: pathID, 724 FolderIsPublic: 0, 725 FolderPublicUpl: 0, 726 FolderPublicDisplay: 0, 727 FolderPublicDnl: 0, 728 } 729 opts := rest.Opts{ 730 Method: "POST", 731 Path: "/folder.json", 732 } 733 resp, err = f.srv.CallJSON(ctx, &opts, &createDirData, &response) 734 return f.shouldRetry(ctx, resp, err) 735 }) 736 if err != nil { 737 return "", err 738 } 739 740 return response.FolderID, nil 741 } 742 743 // FindLeaf finds a directory of name leaf in the folder with ID pathID 744 func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) { 745 // fs.Debugf(nil, "FindLeaf(\"%s\", \"%s\")", pathID, leaf) 746 747 if pathID == "0" && leaf == "" { 748 // fs.Debugf(nil, "Found OpenDrive root") 749 // that's the root directory 750 return pathID, true, nil 751 } 752 753 // get the folderIDs 754 var resp *http.Response 755 folderList := FolderList{} 756 err = f.pacer.Call(func() (bool, error) { 757 opts := rest.Opts{ 758 Method: "GET", 759 Path: "/folder/list.json/" + f.session.SessionID + "/" + pathID, 760 } 761 resp, err = f.srv.CallJSON(ctx, &opts, nil, &folderList) 762 return f.shouldRetry(ctx, resp, err) 763 }) 764 if err != nil { 765 return "", false, fmt.Errorf("failed to get folder list: %w", err) 766 } 767 768 leaf = f.opt.Enc.FromStandardName(leaf) 769 for _, folder := range folderList.Folders { 770 // fs.Debugf(nil, "Folder: %s (%s)", folder.Name, folder.FolderID) 771 772 if strings.EqualFold(leaf, folder.Name) { 773 // found 774 return folder.FolderID, true, nil 775 } 776 } 777 778 return "", false, nil 779 } 780 781 // List the objects and directories in dir into entries. The 782 // entries can be returned in any order but should be for a 783 // complete directory. 784 // 785 // dir should be "" to list the root, and should not have 786 // trailing slashes. 787 // 788 // This should return ErrDirNotFound if the directory isn't 789 // found. 790 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 791 // fs.Debugf(nil, "List(%v)", dir) 792 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 793 if err != nil { 794 return nil, err 795 } 796 797 var resp *http.Response 798 opts := rest.Opts{ 799 Method: "GET", 800 Path: "/folder/list.json/" + f.session.SessionID + "/" + directoryID, 801 } 802 folderList := FolderList{} 803 err = f.pacer.Call(func() (bool, error) { 804 resp, err = f.srv.CallJSON(ctx, &opts, nil, &folderList) 805 return f.shouldRetry(ctx, resp, err) 806 }) 807 if err != nil { 808 if apiError, ok := err.(*Error); ok { 809 // Work around a bug maybe in opendrive or maybe in rclone. 810 // 811 // We should know whether the folder exists or not by the call to 812 // FindDir above so exactly why it is not found here is a mystery. 813 // 814 // This manifests as a failure in fs/sync TestSyncOverlapWithFilter 815 if apiError.Info.Message == "Folder is already deleted" { 816 return fs.DirEntries{}, nil 817 } 818 } 819 return nil, fmt.Errorf("failed to get folder list: %w", err) 820 } 821 822 for _, folder := range folderList.Folders { 823 folder.Name = f.opt.Enc.ToStandardName(folder.Name) 824 // fs.Debugf(nil, "Folder: %s (%s)", folder.Name, folder.FolderID) 825 remote := path.Join(dir, folder.Name) 826 // cache the directory ID for later lookups 827 f.dirCache.Put(remote, folder.FolderID) 828 d := fs.NewDir(remote, time.Unix(folder.DateModified, 0)).SetID(folder.FolderID) 829 d.SetItems(int64(folder.ChildFolders)) 830 d.SetParentID(directoryID) 831 entries = append(entries, d) 832 } 833 834 for _, file := range folderList.Files { 835 file.Name = f.opt.Enc.ToStandardName(file.Name) 836 // fs.Debugf(nil, "File: %s (%s)", file.Name, file.FileID) 837 remote := path.Join(dir, file.Name) 838 o, err := f.newObjectWithInfo(ctx, remote, &file, directoryID) 839 if err != nil { 840 return nil, err 841 } 842 entries = append(entries, o) 843 } 844 845 return entries, nil 846 } 847 848 // ------------------------------------------------------------ 849 850 // Fs returns the parent Fs 851 func (o *Object) Fs() fs.Info { 852 return o.fs 853 } 854 855 // Return a string version 856 func (o *Object) String() string { 857 if o == nil { 858 return "<nil>" 859 } 860 return o.remote 861 } 862 863 // Remote returns the remote path 864 func (o *Object) Remote() string { 865 return o.remote 866 } 867 868 // Hash returns the Md5sum of an object returning a lowercase hex string 869 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 870 if t != hash.MD5 { 871 return "", hash.ErrUnsupported 872 } 873 return o.md5, nil 874 } 875 876 // Size returns the size of an object in bytes 877 func (o *Object) Size() int64 { 878 return o.size // Object is likely PENDING 879 } 880 881 // ModTime returns the modification time of the object 882 // 883 // It attempts to read the objects mtime and if that isn't present the 884 // LastModified returned in the http headers 885 func (o *Object) ModTime(ctx context.Context) time.Time { 886 return o.modTime 887 } 888 889 // SetModTime sets the modification time of the local fs object 890 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 891 // fs.Debugf(nil, "SetModTime(%v)", modTime.String()) 892 opts := rest.Opts{ 893 Method: "PUT", 894 NoResponse: true, 895 Path: "/file/filesettings.json", 896 } 897 update := modTimeFile{ 898 SessionID: o.fs.session.SessionID, 899 FileID: o.id, 900 FileModificationTime: strconv.FormatInt(modTime.Unix(), 10), 901 } 902 err := o.fs.pacer.Call(func() (bool, error) { 903 resp, err := o.fs.srv.CallJSON(ctx, &opts, &update, nil) 904 return o.fs.shouldRetry(ctx, resp, err) 905 }) 906 907 o.modTime = modTime 908 909 return err 910 } 911 912 // Open an object for read 913 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 914 // fs.Debugf(nil, "Open(\"%v\")", o.remote) 915 fs.FixRangeOption(options, o.size) 916 opts := rest.Opts{ 917 Method: "GET", 918 Path: "/download/file.json/" + o.id + "?session_id=" + o.fs.session.SessionID, 919 Options: options, 920 } 921 var resp *http.Response 922 err = o.fs.pacer.Call(func() (bool, error) { 923 resp, err = o.fs.srv.Call(ctx, &opts) 924 return o.fs.shouldRetry(ctx, resp, err) 925 }) 926 if err != nil { 927 return nil, fmt.Errorf("failed to open file): %w", err) 928 } 929 930 return resp.Body, nil 931 } 932 933 // Remove an object 934 func (o *Object) Remove(ctx context.Context) error { 935 // fs.Debugf(nil, "Remove(\"%s\")", o.id) 936 return o.fs.pacer.Call(func() (bool, error) { 937 opts := rest.Opts{ 938 Method: "DELETE", 939 NoResponse: true, 940 Path: "/file.json/" + o.fs.session.SessionID + "/" + o.id, 941 } 942 resp, err := o.fs.srv.Call(ctx, &opts) 943 return o.fs.shouldRetry(ctx, resp, err) 944 }) 945 } 946 947 // Storable returns a boolean showing whether this object storable 948 func (o *Object) Storable() bool { 949 return true 950 } 951 952 // Update the object with the contents of the io.Reader, modTime and size 953 // 954 // The new object may have been created if an error is returned 955 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error { 956 size := src.Size() 957 modTime := src.ModTime(ctx) 958 // fs.Debugf(nil, "Update(\"%s\", \"%s\")", o.id, o.remote) 959 960 // Open file for upload 961 var resp *http.Response 962 openResponse := openUploadResponse{} 963 err := o.fs.pacer.Call(func() (bool, error) { 964 openUploadData := openUpload{SessionID: o.fs.session.SessionID, FileID: o.id, Size: size} 965 // fs.Debugf(nil, "PreOpen: %#v", openUploadData) 966 opts := rest.Opts{ 967 Method: "POST", 968 Options: options, 969 Path: "/upload/open_file_upload.json", 970 } 971 resp, err := o.fs.srv.CallJSON(ctx, &opts, &openUploadData, &openResponse) 972 return o.fs.shouldRetry(ctx, resp, err) 973 }) 974 if err != nil { 975 return fmt.Errorf("failed to create file: %w", err) 976 } 977 // resp.Body.Close() 978 // fs.Debugf(nil, "PostOpen: %#v", openResponse) 979 980 buf := make([]byte, o.fs.opt.ChunkSize) 981 chunkOffset := int64(0) 982 remainingBytes := size 983 chunkCounter := 0 984 985 for remainingBytes > 0 { 986 currentChunkSize := int64(o.fs.opt.ChunkSize) 987 if currentChunkSize > remainingBytes { 988 currentChunkSize = remainingBytes 989 } 990 remainingBytes -= currentChunkSize 991 fs.Debugf(o, "Uploading chunk %d, size=%d, remain=%d", chunkCounter, currentChunkSize, remainingBytes) 992 993 chunk := readers.NewRepeatableLimitReaderBuffer(in, buf, currentChunkSize) 994 var reply uploadFileChunkReply 995 err = o.fs.pacer.Call(func() (bool, error) { 996 // seek to the start in case this is a retry 997 if _, err = chunk.Seek(0, io.SeekStart); err != nil { 998 return false, err 999 } 1000 opts := rest.Opts{ 1001 Method: "POST", 1002 Path: "/upload/upload_file_chunk.json", 1003 Body: chunk, 1004 MultipartParams: url.Values{ 1005 "session_id": []string{o.fs.session.SessionID}, 1006 "file_id": []string{o.id}, 1007 "temp_location": []string{openResponse.TempLocation}, 1008 "chunk_offset": []string{strconv.FormatInt(chunkOffset, 10)}, 1009 "chunk_size": []string{strconv.FormatInt(currentChunkSize, 10)}, 1010 }, 1011 MultipartContentName: "file_data", // ..name of the parameter which is the attached file 1012 MultipartFileName: o.remote, // ..name of the file for the attached file 1013 1014 } 1015 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &reply) 1016 return o.fs.shouldRetry(ctx, resp, err) 1017 }) 1018 if err != nil { 1019 return fmt.Errorf("failed to create file: %w", err) 1020 } 1021 if reply.TotalWritten != currentChunkSize { 1022 return fmt.Errorf("failed to create file: incomplete write of %d/%d bytes", reply.TotalWritten, currentChunkSize) 1023 } 1024 1025 chunkCounter++ 1026 chunkOffset += currentChunkSize 1027 } 1028 1029 // Close file for upload 1030 closeResponse := closeUploadResponse{} 1031 err = o.fs.pacer.Call(func() (bool, error) { 1032 closeUploadData := closeUpload{SessionID: o.fs.session.SessionID, FileID: o.id, Size: size, TempLocation: openResponse.TempLocation} 1033 // fs.Debugf(nil, "PreClose: %#v", closeUploadData) 1034 opts := rest.Opts{ 1035 Method: "POST", 1036 Path: "/upload/close_file_upload.json", 1037 } 1038 resp, err = o.fs.srv.CallJSON(ctx, &opts, &closeUploadData, &closeResponse) 1039 return o.fs.shouldRetry(ctx, resp, err) 1040 }) 1041 if err != nil { 1042 return fmt.Errorf("failed to create file: %w", err) 1043 } 1044 // fs.Debugf(nil, "PostClose: %#v", closeResponse) 1045 1046 o.id = closeResponse.FileID 1047 o.size = closeResponse.Size 1048 1049 // Set the mod time now 1050 err = o.SetModTime(ctx, modTime) 1051 if err != nil { 1052 return err 1053 } 1054 1055 // Set permissions 1056 err = o.fs.pacer.Call(func() (bool, error) { 1057 update := permissions{SessionID: o.fs.session.SessionID, FileID: o.id, FileIsPublic: 0} 1058 // fs.Debugf(nil, "Permissions : %#v", update) 1059 opts := rest.Opts{ 1060 Method: "POST", 1061 NoResponse: true, 1062 Path: "/file/access.json", 1063 } 1064 resp, err = o.fs.srv.CallJSON(ctx, &opts, &update, nil) 1065 return o.fs.shouldRetry(ctx, resp, err) 1066 }) 1067 if err != nil { 1068 return err 1069 } 1070 1071 return o.readMetaData(ctx) 1072 } 1073 1074 func (o *Object) readMetaData(ctx context.Context) (err error) { 1075 leaf, directoryID, err := o.fs.dirCache.FindPath(ctx, o.remote, false) 1076 if err != nil { 1077 if err == fs.ErrorDirNotFound { 1078 return fs.ErrorObjectNotFound 1079 } 1080 return err 1081 } 1082 var resp *http.Response 1083 fileInfo := File{} 1084 1085 // If we know the object id perform a direct lookup 1086 // because the /folder/itembyname.json endpoint is unreliable: 1087 // newly created objects take an arbitrary amount of time to show up 1088 if o.id != "" { 1089 opts := rest.Opts{ 1090 Method: "GET", 1091 Path: fmt.Sprintf("/file/info.json/%s?session_id=%s", 1092 o.id, o.fs.session.SessionID), 1093 } 1094 err = o.fs.pacer.Call(func() (bool, error) { 1095 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &fileInfo) 1096 return o.fs.shouldRetry(ctx, resp, err) 1097 }) 1098 if err != nil { 1099 return fmt.Errorf("failed to get fileinfo: %w", err) 1100 } 1101 1102 o.id = fileInfo.FileID 1103 o.modTime = time.Unix(fileInfo.DateModified, 0) 1104 o.md5 = fileInfo.FileHash 1105 o.size = fileInfo.Size 1106 return nil 1107 } 1108 folderList := FolderList{} 1109 opts := rest.Opts{ 1110 Method: "GET", 1111 Path: fmt.Sprintf("/folder/itembyname.json/%s/%s?name=%s", 1112 o.fs.session.SessionID, directoryID, url.QueryEscape(o.fs.opt.Enc.FromStandardName(leaf))), 1113 } 1114 err = o.fs.pacer.Call(func() (bool, error) { 1115 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &folderList) 1116 return o.fs.shouldRetry(ctx, resp, err) 1117 }) 1118 if err != nil { 1119 return fmt.Errorf("failed to get folder list: %w", err) 1120 } 1121 if len(folderList.Files) == 0 { 1122 return fs.ErrorObjectNotFound 1123 } 1124 fileInfo = folderList.Files[0] 1125 o.id = fileInfo.FileID 1126 o.modTime = time.Unix(fileInfo.DateModified, 0) 1127 o.md5 = fileInfo.FileHash 1128 o.size = fileInfo.Size 1129 return nil 1130 } 1131 1132 // ID returns the ID of the Object if known, or "" if not 1133 func (o *Object) ID() string { 1134 return o.id 1135 } 1136 1137 // ParentID returns the ID of the Object parent directory if known, or "" if not 1138 func (o *Object) ParentID() string { 1139 return o.parent 1140 } 1141 1142 // Check the interfaces are satisfied 1143 var ( 1144 _ fs.Fs = (*Fs)(nil) 1145 _ fs.Purger = (*Fs)(nil) 1146 _ fs.Copier = (*Fs)(nil) 1147 _ fs.Mover = (*Fs)(nil) 1148 _ fs.DirMover = (*Fs)(nil) 1149 _ fs.DirCacheFlusher = (*Fs)(nil) 1150 _ fs.Object = (*Object)(nil) 1151 _ fs.IDer = (*Object)(nil) 1152 _ fs.ParentIDer = (*Object)(nil) 1153 )