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