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