github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/backend/putio/fs.go (about) 1 package putio 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "path" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/pkg/errors" 17 "github.com/putdotio/go-putio/putio" 18 "github.com/rclone/rclone/fs" 19 "github.com/rclone/rclone/fs/config/configmap" 20 "github.com/rclone/rclone/fs/config/configstruct" 21 "github.com/rclone/rclone/fs/hash" 22 "github.com/rclone/rclone/lib/dircache" 23 "github.com/rclone/rclone/lib/oauthutil" 24 "github.com/rclone/rclone/lib/pacer" 25 "github.com/rclone/rclone/lib/readers" 26 ) 27 28 // Fs represents a remote Putio server 29 type Fs struct { 30 name string // name of this remote 31 root string // the path we are working on 32 features *fs.Features // optional features 33 opt Options // options for this Fs 34 client *putio.Client // client for making API calls to Put.io 35 pacer *fs.Pacer // To pace the API calls 36 dirCache *dircache.DirCache // Map of directory path to directory id 37 oAuthClient *http.Client 38 } 39 40 // ------------------------------------------------------------ 41 42 // Name of the remote (as passed into NewFs) 43 func (f *Fs) Name() string { 44 return f.name 45 } 46 47 // Root of the remote (as passed into NewFs) 48 func (f *Fs) Root() string { 49 return f.root 50 } 51 52 // String converts this Fs to a string 53 func (f *Fs) String() string { 54 return fmt.Sprintf("Putio root '%s'", f.root) 55 } 56 57 // Features returns the optional features of this Fs 58 func (f *Fs) Features() *fs.Features { 59 return f.features 60 } 61 62 // NewFs constructs an Fs from the path, container:path 63 func NewFs(name, root string, m configmap.Mapper) (f fs.Fs, err error) { 64 // defer log.Trace(name, "root=%v", root)("f=%+v, err=%v", &f, &err) 65 // Parse config into Options struct 66 opt := new(Options) 67 err = configstruct.Set(m, opt) 68 if err != nil { 69 return nil, err 70 } 71 oAuthClient, _, err := oauthutil.NewClient(name, m, putioConfig) 72 if err != nil { 73 return nil, errors.Wrap(err, "failed to configure putio") 74 } 75 p := &Fs{ 76 name: name, 77 root: root, 78 opt: *opt, 79 pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 80 client: putio.NewClient(oAuthClient), 81 oAuthClient: oAuthClient, 82 } 83 p.features = (&fs.Features{ 84 DuplicateFiles: true, 85 ReadMimeType: true, 86 CanHaveEmptyDirectories: true, 87 }).Fill(p) 88 p.dirCache = dircache.New(root, "0", p) 89 ctx := context.Background() 90 // Find the current root 91 err = p.dirCache.FindRoot(ctx, false) 92 if err != nil { 93 // Assume it is a file 94 newRoot, remote := dircache.SplitPath(root) 95 tempF := *p 96 tempF.dirCache = dircache.New(newRoot, "0", &tempF) 97 tempF.root = newRoot 98 // Make new Fs which is the parent 99 err = tempF.dirCache.FindRoot(ctx, false) 100 if err != nil { 101 // No root so return old f 102 return p, nil 103 } 104 _, err := tempF.NewObject(ctx, remote) 105 if err != nil { 106 // unable to list folder so return old f 107 return p, nil 108 } 109 // XXX: update the old f here instead of returning tempF, since 110 // `features` were already filled with functions having *f as a receiver. 111 // See https://github.com/rclone/rclone/issues/2182 112 p.dirCache = tempF.dirCache 113 p.root = tempF.root 114 return p, fs.ErrorIsFile 115 } 116 // fs.Debugf(p, "Root id: %s", p.dirCache.RootID()) 117 return p, nil 118 } 119 120 func itoa(i int64) string { 121 return strconv.FormatInt(i, 10) 122 } 123 124 func atoi(a string) int64 { 125 i, err := strconv.ParseInt(a, 10, 64) 126 if err != nil { 127 panic(err) 128 } 129 return i 130 } 131 132 // CreateDir makes a directory with pathID as parent and name leaf 133 func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) { 134 // defer log.Trace(f, "pathID=%v, leaf=%v", pathID, leaf)("newID=%v, err=%v", newID, &err) 135 parentID := atoi(pathID) 136 var entry putio.File 137 err = f.pacer.Call(func() (bool, error) { 138 // fs.Debugf(f, "creating folder. part: %s, parentID: %d", leaf, parentID) 139 entry, err = f.client.Files.CreateFolder(ctx, f.opt.Enc.FromStandardName(leaf), parentID) 140 return shouldRetry(err) 141 }) 142 return itoa(entry.ID), err 143 } 144 145 // FindLeaf finds a directory of name leaf in the folder with ID pathID 146 func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) { 147 // defer log.Trace(f, "pathID=%v, leaf=%v", pathID, leaf)("pathIDOut=%v, found=%v, err=%v", pathIDOut, found, &err) 148 if pathID == "0" && leaf == "" { 149 // that's the root directory 150 return pathID, true, nil 151 } 152 fileID := atoi(pathID) 153 var children []putio.File 154 err = f.pacer.Call(func() (bool, error) { 155 // fs.Debugf(f, "listing file: %d", fileID) 156 children, _, err = f.client.Files.List(ctx, fileID) 157 return shouldRetry(err) 158 }) 159 if err != nil { 160 if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode == 404 { 161 err = nil 162 } 163 return 164 } 165 for _, child := range children { 166 if f.opt.Enc.ToStandardName(child.Name) == leaf { 167 found = true 168 pathIDOut = itoa(child.ID) 169 if !child.IsDir() { 170 err = fs.ErrorIsFile 171 } 172 return 173 } 174 } 175 return 176 } 177 178 // List the objects and directories in dir into entries. The 179 // entries can be returned in any order but should be for a 180 // complete directory. 181 // 182 // dir should be "" to list the root, and should not have 183 // trailing slashes. 184 // 185 // This should return ErrDirNotFound if the directory isn't 186 // found. 187 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 188 // defer log.Trace(f, "dir=%v", dir)("err=%v", &err) 189 err = f.dirCache.FindRoot(ctx, false) 190 if err != nil { 191 return nil, err 192 } 193 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 194 if err != nil { 195 return nil, err 196 } 197 parentID := atoi(directoryID) 198 var children []putio.File 199 err = f.pacer.Call(func() (bool, error) { 200 // fs.Debugf(f, "listing files inside List: %d", parentID) 201 children, _, err = f.client.Files.List(ctx, parentID) 202 return shouldRetry(err) 203 }) 204 if err != nil { 205 return 206 } 207 for _, child := range children { 208 remote := path.Join(dir, f.opt.Enc.ToStandardName(child.Name)) 209 // fs.Debugf(f, "child: %s", remote) 210 if child.IsDir() { 211 f.dirCache.Put(remote, itoa(child.ID)) 212 d := fs.NewDir(remote, child.UpdatedAt.Time) 213 entries = append(entries, d) 214 } else { 215 o, err := f.newObjectWithInfo(ctx, remote, child) 216 if err != nil { 217 return nil, err 218 } 219 entries = append(entries, o) 220 } 221 } 222 return 223 } 224 225 // Put the object 226 // 227 // Copy the reader in to the new object which is returned 228 // 229 // The new object may have been created if an error is returned 230 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (o fs.Object, err error) { 231 // defer log.Trace(f, "src=%+v", src)("o=%+v, err=%v", &o, &err) 232 exisitingObj, err := f.NewObject(ctx, src.Remote()) 233 switch err { 234 case nil: 235 return exisitingObj, exisitingObj.Update(ctx, in, src, options...) 236 case fs.ErrorObjectNotFound: 237 // Not found so create it 238 return f.PutUnchecked(ctx, in, src, options...) 239 default: 240 return nil, err 241 } 242 } 243 244 // PutUnchecked uploads the object 245 // 246 // This will create a duplicate if we upload a new file without 247 // checking to see if there is one already - use Put() for that. 248 func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (o fs.Object, err error) { 249 // defer log.Trace(f, "src=%+v", src)("o=%+v, err=%v", &o, &err) 250 size := src.Size() 251 remote := src.Remote() 252 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, true) 253 if err != nil { 254 return nil, err 255 } 256 loc, err := f.createUpload(ctx, leaf, size, directoryID, src.ModTime(ctx)) 257 if err != nil { 258 return nil, err 259 } 260 fileID, err := f.sendUpload(ctx, loc, size, in) 261 if err != nil { 262 return nil, err 263 } 264 var entry putio.File 265 err = f.pacer.Call(func() (bool, error) { 266 // fs.Debugf(f, "getting file: %d", fileID) 267 entry, err = f.client.Files.Get(ctx, fileID) 268 return shouldRetry(err) 269 }) 270 if err != nil { 271 return nil, err 272 } 273 return f.newObjectWithInfo(ctx, remote, entry) 274 } 275 276 func (f *Fs) createUpload(ctx context.Context, name string, size int64, parentID string, modTime time.Time) (location string, err error) { 277 // defer log.Trace(f, "name=%v, size=%v, parentID=%v, modTime=%v", name, size, parentID, modTime.String())("location=%v, err=%v", location, &err) 278 err = f.pacer.Call(func() (bool, error) { 279 req, err := http.NewRequest("POST", "https://upload.put.io/files/", nil) 280 if err != nil { 281 return false, err 282 } 283 req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext 284 req.Header.Set("tus-resumable", "1.0.0") 285 req.Header.Set("upload-length", strconv.FormatInt(size, 10)) 286 b64name := base64.StdEncoding.EncodeToString([]byte(f.opt.Enc.FromStandardName(name))) 287 b64true := base64.StdEncoding.EncodeToString([]byte("true")) 288 b64parentID := base64.StdEncoding.EncodeToString([]byte(parentID)) 289 b64modifiedAt := base64.StdEncoding.EncodeToString([]byte(modTime.Format(time.RFC3339))) 290 req.Header.Set("upload-metadata", fmt.Sprintf("name %s,no-torrent %s,parent_id %s,updated-at %s", b64name, b64true, b64parentID, b64modifiedAt)) 291 resp, err := f.oAuthClient.Do(req) 292 retry, err := shouldRetry(err) 293 if retry { 294 return true, err 295 } 296 if err != nil { 297 return false, err 298 } 299 if resp.StatusCode != 201 { 300 return false, fmt.Errorf("unexpected status code from upload create: %d", resp.StatusCode) 301 } 302 location = resp.Header.Get("location") 303 if location == "" { 304 return false, errors.New("empty location header from upload create") 305 } 306 return false, nil 307 }) 308 return 309 } 310 311 func (f *Fs) sendUpload(ctx context.Context, location string, size int64, in io.Reader) (fileID int64, err error) { 312 // defer log.Trace(f, "location=%v, size=%v", location, size)("fileID=%v, err=%v", &fileID, &err) 313 if size == 0 { 314 err = f.pacer.Call(func() (bool, error) { 315 fs.Debugf(f, "Sending zero length chunk") 316 _, fileID, err = f.transferChunk(ctx, location, 0, bytes.NewReader([]byte{}), 0) 317 return shouldRetry(err) 318 }) 319 return 320 } 321 var clientOffset int64 322 var offsetMismatch bool 323 buf := make([]byte, defaultChunkSize) 324 for clientOffset < size { 325 chunkSize := size - clientOffset 326 if chunkSize >= int64(defaultChunkSize) { 327 chunkSize = int64(defaultChunkSize) 328 } 329 chunk := readers.NewRepeatableLimitReaderBuffer(in, buf, chunkSize) 330 chunkStart := clientOffset 331 reqSize := chunkSize 332 transferOffset := clientOffset 333 fs.Debugf(f, "chunkStart: %d, reqSize: %d", chunkStart, reqSize) 334 335 // Transfer the chunk 336 err = f.pacer.Call(func() (bool, error) { 337 if offsetMismatch { 338 // Get file offset and seek to the position 339 offset, err := f.getServerOffset(ctx, location) 340 if err != nil { 341 return shouldRetry(err) 342 } 343 sentBytes := offset - chunkStart 344 fs.Debugf(f, "sentBytes: %d", sentBytes) 345 _, err = chunk.Seek(sentBytes, io.SeekStart) 346 if err != nil { 347 return shouldRetry(err) 348 } 349 transferOffset = offset 350 reqSize = chunkSize - sentBytes 351 offsetMismatch = false 352 } 353 fs.Debugf(f, "Sending chunk. transferOffset: %d length: %d", transferOffset, reqSize) 354 var serverOffset int64 355 serverOffset, fileID, err = f.transferChunk(ctx, location, transferOffset, chunk, reqSize) 356 if cerr, ok := err.(*statusCodeError); ok && cerr.response.StatusCode == 409 { 357 offsetMismatch = true 358 return true, err 359 } 360 if serverOffset != (transferOffset + reqSize) { 361 offsetMismatch = true 362 return true, errors.New("connection broken") 363 } 364 return shouldRetry(err) 365 }) 366 if err != nil { 367 return 368 } 369 370 clientOffset += chunkSize 371 } 372 return 373 } 374 375 func (f *Fs) getServerOffset(ctx context.Context, location string) (offset int64, err error) { 376 // defer log.Trace(f, "location=%v", location)("offset=%v, err=%v", &offset, &err) 377 req, err := f.makeUploadHeadRequest(ctx, location) 378 if err != nil { 379 return 0, err 380 } 381 resp, err := f.oAuthClient.Do(req) 382 if err != nil { 383 return 0, err 384 } 385 err = checkStatusCode(resp, 200) 386 if err != nil { 387 return 0, err 388 } 389 return strconv.ParseInt(resp.Header.Get("upload-offset"), 10, 64) 390 } 391 392 func (f *Fs) transferChunk(ctx context.Context, location string, start int64, chunk io.ReadSeeker, chunkSize int64) (serverOffset, fileID int64, err error) { 393 // defer log.Trace(f, "location=%v, start=%v, chunkSize=%v", location, start, chunkSize)("fileID=%v, err=%v", &fileID, &err) 394 req, err := f.makeUploadPatchRequest(ctx, location, chunk, start, chunkSize) 395 if err != nil { 396 return 397 } 398 resp, err := f.oAuthClient.Do(req) 399 if err != nil { 400 return 401 } 402 defer func() { 403 _ = resp.Body.Close() 404 }() 405 err = checkStatusCode(resp, 204) 406 if err != nil { 407 return 408 } 409 serverOffset, err = strconv.ParseInt(resp.Header.Get("upload-offset"), 10, 64) 410 if err != nil { 411 return 412 } 413 sfid := resp.Header.Get("putio-file-id") 414 if sfid != "" { 415 fileID, err = strconv.ParseInt(sfid, 10, 64) 416 if err != nil { 417 return 418 } 419 } 420 return 421 } 422 423 func (f *Fs) makeUploadHeadRequest(ctx context.Context, location string) (*http.Request, error) { 424 req, err := http.NewRequest("HEAD", location, nil) 425 if err != nil { 426 return nil, err 427 } 428 req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext 429 req.Header.Set("tus-resumable", "1.0.0") 430 return req, nil 431 } 432 433 func (f *Fs) makeUploadPatchRequest(ctx context.Context, location string, in io.Reader, offset, length int64) (*http.Request, error) { 434 req, err := http.NewRequest("PATCH", location, in) 435 if err != nil { 436 return nil, err 437 } 438 req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext 439 req.Header.Set("tus-resumable", "1.0.0") 440 req.Header.Set("upload-offset", strconv.FormatInt(offset, 10)) 441 req.Header.Set("content-length", strconv.FormatInt(length, 10)) 442 req.Header.Set("content-type", "application/offset+octet-stream") 443 return req, nil 444 } 445 446 // Mkdir creates the container if it doesn't exist 447 func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) { 448 // defer log.Trace(f, "dir=%v", dir)("err=%v", &err) 449 err = f.dirCache.FindRoot(ctx, true) 450 if err != nil { 451 return err 452 } 453 if dir != "" { 454 _, err = f.dirCache.FindDir(ctx, dir, true) 455 } 456 return err 457 } 458 459 // Rmdir deletes the container 460 // 461 // Returns an error if it isn't empty 462 func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) { 463 // defer log.Trace(f, "dir=%v", dir)("err=%v", &err) 464 465 root := strings.Trim(path.Join(f.root, dir), "/") 466 467 // can't remove root 468 if root == "" { 469 return errors.New("can't remove root directory") 470 } 471 472 // check directory exists 473 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 474 if err != nil { 475 return errors.Wrap(err, "Rmdir") 476 } 477 dirID := atoi(directoryID) 478 479 // check directory empty 480 var children []putio.File 481 err = f.pacer.Call(func() (bool, error) { 482 // fs.Debugf(f, "listing files: %d", dirID) 483 children, _, err = f.client.Files.List(ctx, dirID) 484 return shouldRetry(err) 485 }) 486 if err != nil { 487 return errors.Wrap(err, "Rmdir") 488 } 489 if len(children) != 0 { 490 return errors.New("directory not empty") 491 } 492 493 // remove it 494 err = f.pacer.Call(func() (bool, error) { 495 // fs.Debugf(f, "deleting file: %d", dirID) 496 err = f.client.Files.Delete(ctx, dirID) 497 return shouldRetry(err) 498 }) 499 f.dirCache.FlushDir(dir) 500 return err 501 } 502 503 // Precision returns the precision 504 func (f *Fs) Precision() time.Duration { 505 return time.Second 506 } 507 508 // Purge deletes all the files and the container 509 // 510 // Optional interface: Only implement this if you have a way of 511 // deleting all the files quicker than just running Remove() on the 512 // result of List() 513 func (f *Fs) Purge(ctx context.Context) (err error) { 514 // defer log.Trace(f, "")("err=%v", &err) 515 516 if f.root == "" { 517 return errors.New("can't purge root directory") 518 } 519 err = f.dirCache.FindRoot(ctx, false) 520 if err != nil { 521 return err 522 } 523 524 rootID := atoi(f.dirCache.RootID()) 525 // Let putio delete the filesystem tree 526 err = f.pacer.Call(func() (bool, error) { 527 // fs.Debugf(f, "deleting file: %d", rootID) 528 err = f.client.Files.Delete(ctx, rootID) 529 return shouldRetry(err) 530 }) 531 f.dirCache.ResetRoot() 532 return err 533 } 534 535 // Copy src to this remote using server side copy operations. 536 // 537 // This is stored with the remote path given 538 // 539 // It returns the destination Object and a possible error 540 // 541 // Will only be called if src.Fs().Name() == f.Name() 542 // 543 // If it isn't possible then return fs.ErrorCantCopy 544 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (o fs.Object, err error) { 545 // defer log.Trace(f, "src=%+v, remote=%v", src, remote)("o=%+v, err=%v", &o, &err) 546 srcObj, ok := src.(*Object) 547 if !ok { 548 return nil, fs.ErrorCantCopy 549 } 550 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, true) 551 if err != nil { 552 return nil, err 553 } 554 err = f.pacer.Call(func() (bool, error) { 555 params := url.Values{} 556 params.Set("file_id", strconv.FormatInt(srcObj.file.ID, 10)) 557 params.Set("parent_id", directoryID) 558 params.Set("name", f.opt.Enc.FromStandardName(leaf)) 559 req, err := f.client.NewRequest(ctx, "POST", "/v2/files/copy", strings.NewReader(params.Encode())) 560 if err != nil { 561 return false, err 562 } 563 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 564 // fs.Debugf(f, "copying file (%d) to parent_id: %s", srcObj.file.ID, directoryID) 565 _, err = f.client.Do(req, nil) 566 return shouldRetry(err) 567 }) 568 if err != nil { 569 return nil, err 570 } 571 return f.NewObject(ctx, remote) 572 } 573 574 // Move src to this remote using server side move operations. 575 // 576 // This is stored with the remote path given 577 // 578 // It returns the destination Object and a possible error 579 // 580 // Will only be called if src.Fs().Name() == f.Name() 581 // 582 // If it isn't possible then return fs.ErrorCantMove 583 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (o fs.Object, err error) { 584 // defer log.Trace(f, "src=%+v, remote=%v", src, remote)("o=%+v, err=%v", &o, &err) 585 srcObj, ok := src.(*Object) 586 if !ok { 587 return nil, fs.ErrorCantMove 588 } 589 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, true) 590 if err != nil { 591 return nil, err 592 } 593 err = f.pacer.Call(func() (bool, error) { 594 params := url.Values{} 595 params.Set("file_id", strconv.FormatInt(srcObj.file.ID, 10)) 596 params.Set("parent_id", directoryID) 597 params.Set("name", f.opt.Enc.FromStandardName(leaf)) 598 req, err := f.client.NewRequest(ctx, "POST", "/v2/files/move", strings.NewReader(params.Encode())) 599 if err != nil { 600 return false, err 601 } 602 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 603 // fs.Debugf(f, "moving file (%d) to parent_id: %s", srcObj.file.ID, directoryID) 604 _, err = f.client.Do(req, nil) 605 return shouldRetry(err) 606 }) 607 if err != nil { 608 return nil, err 609 } 610 return f.NewObject(ctx, remote) 611 } 612 613 // DirMove moves src, srcRemote to this remote at dstRemote 614 // using server side move operations. 615 // 616 // Will only be called if src.Fs().Name() == f.Name() 617 // 618 // If it isn't possible then return fs.ErrorCantDirMove 619 // 620 // If destination exists then return fs.ErrorDirExists 621 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) (err error) { 622 // defer log.Trace(f, "src=%+v, srcRemote=%v, dstRemote", src, srcRemote, dstRemote)("err=%v", &err) 623 srcFs, ok := src.(*Fs) 624 if !ok { 625 return fs.ErrorCantDirMove 626 } 627 srcPath := path.Join(srcFs.root, srcRemote) 628 dstPath := path.Join(f.root, dstRemote) 629 630 // Refuse to move to or from the root 631 if srcPath == "" || dstPath == "" { 632 return errors.New("can't move root directory") 633 } 634 635 // find the root src directory 636 err = srcFs.dirCache.FindRoot(ctx, false) 637 if err != nil { 638 return err 639 } 640 641 // find the root dst directory 642 if dstRemote != "" { 643 err = f.dirCache.FindRoot(ctx, true) 644 if err != nil { 645 return err 646 } 647 } else { 648 if f.dirCache.FoundRoot() { 649 return fs.ErrorDirExists 650 } 651 } 652 653 // Find ID of dst parent, creating subdirs if necessary 654 var leaf, dstDirectoryID string 655 findPath := dstRemote 656 if dstRemote == "" { 657 findPath = f.root 658 } 659 leaf, dstDirectoryID, err = f.dirCache.FindPath(ctx, findPath, true) 660 if err != nil { 661 return err 662 } 663 664 // Check destination does not exist 665 if dstRemote != "" { 666 _, err = f.dirCache.FindDir(ctx, dstRemote, false) 667 if err == fs.ErrorDirNotFound { 668 // OK 669 } else if err != nil { 670 return err 671 } else { 672 return fs.ErrorDirExists 673 } 674 } 675 676 // Find ID of src 677 srcID, err := srcFs.dirCache.FindDir(ctx, srcRemote, false) 678 if err != nil { 679 return err 680 } 681 682 err = f.pacer.Call(func() (bool, error) { 683 params := url.Values{} 684 params.Set("file_id", srcID) 685 params.Set("parent_id", dstDirectoryID) 686 params.Set("name", f.opt.Enc.FromStandardName(leaf)) 687 req, err := f.client.NewRequest(ctx, "POST", "/v2/files/move", strings.NewReader(params.Encode())) 688 if err != nil { 689 return false, err 690 } 691 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 692 // fs.Debugf(f, "moving file (%s) to parent_id: %s", srcID, dstDirectoryID) 693 _, err = f.client.Do(req, nil) 694 return shouldRetry(err) 695 }) 696 srcFs.dirCache.FlushDir(srcRemote) 697 return err 698 } 699 700 // About gets quota information 701 func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) { 702 // defer log.Trace(f, "")("usage=%+v, err=%v", usage, &err) 703 var ai putio.AccountInfo 704 err = f.pacer.Call(func() (bool, error) { 705 // fs.Debugf(f, "getting account info") 706 ai, err = f.client.Account.Info(ctx) 707 return shouldRetry(err) 708 }) 709 if err != nil { 710 return nil, errors.Wrap(err, "about failed") 711 } 712 return &fs.Usage{ 713 Total: fs.NewUsageValue(ai.Disk.Size), // quota of bytes that can be used 714 Used: fs.NewUsageValue(ai.Disk.Used), // bytes in use 715 Free: fs.NewUsageValue(ai.Disk.Avail), // bytes which can be uploaded before reaching the quota 716 }, nil 717 } 718 719 // Hashes returns the supported hash sets. 720 func (f *Fs) Hashes() hash.Set { 721 return hash.Set(hash.CRC32) 722 } 723 724 // DirCacheFlush resets the directory cache - used in testing as an 725 // optional interface 726 func (f *Fs) DirCacheFlush() { 727 // defer log.Trace(f, "")("") 728 f.dirCache.ResetRoot() 729 } 730 731 // CleanUp the trash in the Fs 732 func (f *Fs) CleanUp(ctx context.Context) (err error) { 733 // defer log.Trace(f, "")("err=%v", &err) 734 return f.pacer.Call(func() (bool, error) { 735 req, err := f.client.NewRequest(ctx, "POST", "/v2/trash/empty", nil) 736 if err != nil { 737 return false, err 738 } 739 // fs.Debugf(f, "emptying trash") 740 _, err = f.client.Do(req, nil) 741 return shouldRetry(err) 742 }) 743 }