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  }