github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/linkbox/linkbox.go (about)

     1  // Package linkbox provides an interface to the linkbox.to Cloud storage system.
     2  //
     3  // API docs: https://www.linkbox.to/api-docs
     4  package linkbox
     5  
     6  /*
     7     Extras
     8     - PublicLink - NO - sharing doesn't share the actual file, only a page with it on
     9     - Move - YES - have Move and Rename file APIs so is possible
    10     - MoveDir - NO - probably not possible - have Move but no Rename
    11  */
    12  
    13  import (
    14  	"bytes"
    15  	"context"
    16  	"crypto/md5"
    17  	"fmt"
    18  	"io"
    19  	"net/http"
    20  	"net/url"
    21  	"path"
    22  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/rclone/rclone/fs"
    28  	"github.com/rclone/rclone/fs/config/configmap"
    29  	"github.com/rclone/rclone/fs/config/configstruct"
    30  	"github.com/rclone/rclone/fs/fserrors"
    31  	"github.com/rclone/rclone/fs/fshttp"
    32  	"github.com/rclone/rclone/fs/hash"
    33  	"github.com/rclone/rclone/lib/dircache"
    34  	"github.com/rclone/rclone/lib/pacer"
    35  	"github.com/rclone/rclone/lib/rest"
    36  )
    37  
    38  const (
    39  	maxEntitiesPerPage = 1000
    40  	minSleep           = 200 * time.Millisecond
    41  	maxSleep           = 2 * time.Second
    42  	pacerBurst         = 1
    43  	linkboxAPIURL      = "https://www.linkbox.to/api/open/"
    44  	rootID             = "0" // ID of root directory
    45  )
    46  
    47  func init() {
    48  	fsi := &fs.RegInfo{
    49  		Name:        "linkbox",
    50  		Description: "Linkbox",
    51  		NewFs:       NewFs,
    52  		Options: []fs.Option{{
    53  			Name:      "token",
    54  			Help:      "Token from https://www.linkbox.to/admin/account",
    55  			Sensitive: true,
    56  			Required:  true,
    57  		}},
    58  	}
    59  	fs.Register(fsi)
    60  }
    61  
    62  // Options defines the configuration for this backend
    63  type Options struct {
    64  	Token string `config:"token"`
    65  }
    66  
    67  // Fs stores the interface to the remote Linkbox files
    68  type Fs struct {
    69  	name     string
    70  	root     string
    71  	opt      Options            // options for this backend
    72  	features *fs.Features       // optional features
    73  	ci       *fs.ConfigInfo     // global config
    74  	srv      *rest.Client       // the connection to the server
    75  	dirCache *dircache.DirCache // Map of directory path to directory id
    76  	pacer    *fs.Pacer
    77  }
    78  
    79  // Object is a remote object that has been stat'd (so it exists, but is not necessarily open for reading)
    80  type Object struct {
    81  	fs          *Fs
    82  	remote      string
    83  	size        int64
    84  	modTime     time.Time
    85  	contentType string
    86  	fullURL     string
    87  	dirID       int64
    88  	itemID      string // and these IDs are for files
    89  	id          int64  // these IDs appear to apply to directories
    90  	isDir       bool
    91  }
    92  
    93  // NewFs creates a new Fs object from the name and root. It connects to
    94  // the host specified in the config file.
    95  func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
    96  	root = strings.Trim(root, "/")
    97  	// Parse config into Options struct
    98  
    99  	opt := new(Options)
   100  	err := configstruct.Set(m, opt)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	ci := fs.GetConfig(ctx)
   106  
   107  	f := &Fs{
   108  		name: name,
   109  		opt:  *opt,
   110  		root: root,
   111  		ci:   ci,
   112  		srv:  rest.NewClient(fshttp.NewClient(ctx)),
   113  
   114  		pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep))),
   115  	}
   116  	f.dirCache = dircache.New(root, rootID, f)
   117  
   118  	f.features = (&fs.Features{
   119  		CanHaveEmptyDirectories: true,
   120  		CaseInsensitive:         true,
   121  	}).Fill(ctx, f)
   122  
   123  	// Find the current root
   124  	err = f.dirCache.FindRoot(ctx, false)
   125  	if err != nil {
   126  		// Assume it is a file
   127  		newRoot, remote := dircache.SplitPath(root)
   128  		tempF := *f
   129  		tempF.dirCache = dircache.New(newRoot, rootID, &tempF)
   130  		tempF.root = newRoot
   131  		// Make new Fs which is the parent
   132  		err = tempF.dirCache.FindRoot(ctx, false)
   133  		if err != nil {
   134  			// No root so return old f
   135  			return f, nil
   136  		}
   137  		_, err := tempF.NewObject(ctx, remote)
   138  		if err != nil {
   139  			if err == fs.ErrorObjectNotFound {
   140  				// File doesn't exist so return old f
   141  				return f, nil
   142  			}
   143  			return nil, err
   144  		}
   145  		f.features.Fill(ctx, &tempF)
   146  		// XXX: update the old f here instead of returning tempF, since
   147  		// `features` were already filled with functions having *f as a receiver.
   148  		// See https://github.com/rclone/rclone/issues/2182
   149  		f.dirCache = tempF.dirCache
   150  		f.root = tempF.root
   151  		// return an error with an fs which points to the parent
   152  		return f, fs.ErrorIsFile
   153  	}
   154  	return f, nil
   155  }
   156  
   157  type entity struct {
   158  	Type   string `json:"type"`
   159  	Name   string `json:"name"`
   160  	URL    string `json:"url"`
   161  	Ctime  int64  `json:"ctime"`
   162  	Size   int64  `json:"size"`
   163  	ID     int64  `json:"id"`
   164  	Pid    int64  `json:"pid"`
   165  	ItemID string `json:"item_id"`
   166  }
   167  
   168  // Return true if the entity is a directory
   169  func (e *entity) isDir() bool {
   170  	return e.Type == "dir" || e.Type == "sdir"
   171  }
   172  
   173  type data struct {
   174  	Entities []entity `json:"list"`
   175  }
   176  type fileSearchRes struct {
   177  	response
   178  	SearchData data `json:"data"`
   179  }
   180  
   181  // Set an object info from an entity
   182  func (o *Object) set(e *entity) {
   183  	o.modTime = time.Unix(e.Ctime, 0)
   184  	o.contentType = e.Type
   185  	o.size = e.Size
   186  	o.fullURL = e.URL
   187  	o.isDir = e.isDir()
   188  	o.id = e.ID
   189  	o.itemID = e.ItemID
   190  	o.dirID = e.Pid
   191  }
   192  
   193  // Call linkbox with the query in opts and return result
   194  //
   195  // This will be checked for error and an error will be returned if Status != 1
   196  func getUnmarshaledResponse(ctx context.Context, f *Fs, opts *rest.Opts, result interface{}) error {
   197  	err := f.pacer.Call(func() (bool, error) {
   198  		resp, err := f.srv.CallJSON(ctx, opts, nil, &result)
   199  		return f.shouldRetry(ctx, resp, err)
   200  	})
   201  	if err != nil {
   202  		return err
   203  	}
   204  	responser := result.(responser)
   205  	if responser.IsError() {
   206  		return responser
   207  	}
   208  	return nil
   209  }
   210  
   211  // list the objects into the function supplied
   212  //
   213  // If directories is set it only sends directories
   214  // User function to process a File item from listAll
   215  //
   216  // Should return true to finish processing
   217  type listAllFn func(*entity) bool
   218  
   219  // Search is a bit fussy about which characters match
   220  //
   221  // If the name doesn't match this then do an dir list instead
   222  // N.B.: Linkbox doesn't support search by name that is longer than 50 chars
   223  var searchOK = regexp.MustCompile(`^[a-zA-Z0-9_ -.]{1,50}$`)
   224  
   225  // Lists the directory required calling the user function on each item found
   226  //
   227  // If the user fn ever returns true then it early exits with found = true
   228  //
   229  // If you set name then search ignores dirID. name is a substring
   230  // search also so name="dir" matches "sub dir" also. This filters it
   231  // down so it only returns items in dirID
   232  func (f *Fs) listAll(ctx context.Context, dirID string, name string, fn listAllFn) (found bool, err error) {
   233  	var (
   234  		pageNumber       = 0
   235  		numberOfEntities = maxEntitiesPerPage
   236  	)
   237  	name = strings.TrimSpace(name) // search doesn't like spaces
   238  	if !searchOK.MatchString(name) {
   239  		// If name isn't good then do an unbounded search
   240  		name = ""
   241  	}
   242  
   243  OUTER:
   244  	for numberOfEntities == maxEntitiesPerPage {
   245  		pageNumber++
   246  		opts := &rest.Opts{
   247  			Method:  "GET",
   248  			RootURL: linkboxAPIURL,
   249  			Path:    "file_search",
   250  			Parameters: url.Values{
   251  				"token":    {f.opt.Token},
   252  				"name":     {name},
   253  				"pid":      {dirID},
   254  				"pageNo":   {itoa(pageNumber)},
   255  				"pageSize": {itoa64(maxEntitiesPerPage)},
   256  			},
   257  		}
   258  
   259  		var responseResult fileSearchRes
   260  		err = getUnmarshaledResponse(ctx, f, opts, &responseResult)
   261  		if err != nil {
   262  			return false, fmt.Errorf("getting files failed: %w", err)
   263  		}
   264  
   265  		numberOfEntities = len(responseResult.SearchData.Entities)
   266  
   267  		for _, entity := range responseResult.SearchData.Entities {
   268  			if itoa64(entity.Pid) != dirID {
   269  				// when name != "" this returns from all directories, so ignore not this one
   270  				continue
   271  			}
   272  			if fn(&entity) {
   273  				found = true
   274  				break OUTER
   275  			}
   276  		}
   277  		if pageNumber > 100000 {
   278  			return false, fmt.Errorf("too many results")
   279  		}
   280  	}
   281  	return found, nil
   282  }
   283  
   284  // Turn 64 bit int to string
   285  func itoa64(i int64) string {
   286  	return strconv.FormatInt(i, 10)
   287  }
   288  
   289  // Turn int to string
   290  func itoa(i int) string {
   291  	return itoa64(int64(i))
   292  }
   293  
   294  func splitDirAndName(remote string) (dir string, name string) {
   295  	lastSlashPosition := strings.LastIndex(remote, "/")
   296  	if lastSlashPosition == -1 {
   297  		dir = ""
   298  		name = remote
   299  	} else {
   300  		dir = remote[:lastSlashPosition]
   301  		name = remote[lastSlashPosition+1:]
   302  	}
   303  
   304  	// fs.Debugf(nil, "splitDirAndName remote = {%s}, dir = {%s}, name = {%s}", remote, dir, name)
   305  
   306  	return dir, name
   307  }
   308  
   309  // FindLeaf finds a directory of name leaf in the folder with ID directoryID
   310  func (f *Fs) FindLeaf(ctx context.Context, directoryID, leaf string) (directoryIDOut string, found bool, err error) {
   311  	// Find the leaf in directoryID
   312  	found, err = f.listAll(ctx, directoryID, leaf, func(entity *entity) bool {
   313  		if entity.isDir() && strings.EqualFold(entity.Name, leaf) {
   314  			directoryIDOut = itoa64(entity.ID)
   315  			return true
   316  		}
   317  		return false
   318  	})
   319  	return directoryIDOut, found, err
   320  }
   321  
   322  // Returned from "folder_create"
   323  type folderCreateRes struct {
   324  	response
   325  	Data struct {
   326  		DirID int64 `json:"dirId"`
   327  	} `json:"data"`
   328  }
   329  
   330  // CreateDir makes a directory with dirID as parent and name leaf
   331  func (f *Fs) CreateDir(ctx context.Context, dirID, leaf string) (newID string, err error) {
   332  	// fs.Debugf(f, "CreateDir(%q, %q)\n", dirID, leaf)
   333  	opts := &rest.Opts{
   334  		Method:  "GET",
   335  		RootURL: linkboxAPIURL,
   336  		Path:    "folder_create",
   337  		Parameters: url.Values{
   338  			"token":       {f.opt.Token},
   339  			"name":        {leaf},
   340  			"pid":         {dirID},
   341  			"isShare":     {"0"},
   342  			"canInvite":   {"1"},
   343  			"canShare":    {"1"},
   344  			"withBodyImg": {"1"},
   345  			"desc":        {""},
   346  		},
   347  	}
   348  
   349  	response := folderCreateRes{}
   350  	err = getUnmarshaledResponse(ctx, f, opts, &response)
   351  	if err != nil {
   352  		// response status 1501 means that directory already exists
   353  		if response.Status == 1501 {
   354  			return newID, fmt.Errorf("couldn't find already created directory: %w", fs.ErrorDirNotFound)
   355  		}
   356  		return newID, fmt.Errorf("CreateDir failed: %w", err)
   357  
   358  	}
   359  	if response.Data.DirID == 0 {
   360  		return newID, fmt.Errorf("API returned 0 for ID of newly created directory")
   361  	}
   362  	return itoa64(response.Data.DirID), nil
   363  }
   364  
   365  // List the objects and directories in dir into entries.  The
   366  // entries can be returned in any order but should be for a
   367  // complete directory.
   368  //
   369  // dir should be "" to list the root, and should not have
   370  // trailing slashes.
   371  //
   372  // This should return ErrDirNotFound if the directory isn't
   373  // found.
   374  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   375  	// fs.Debugf(f, "List method dir = {%s}", dir)
   376  	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  	_, err = f.listAll(ctx, directoryID, "", func(entity *entity) bool {
   381  		remote := path.Join(dir, entity.Name)
   382  		if entity.isDir() {
   383  			id := itoa64(entity.ID)
   384  			modTime := time.Unix(entity.Ctime, 0)
   385  			d := fs.NewDir(remote, modTime).SetID(id).SetParentID(itoa64(entity.Pid))
   386  			entries = append(entries, d)
   387  			// cache the directory ID for later lookups
   388  			f.dirCache.Put(remote, id)
   389  		} else {
   390  			o := &Object{
   391  				fs:     f,
   392  				remote: remote,
   393  			}
   394  			o.set(entity)
   395  			entries = append(entries, o)
   396  		}
   397  		return false
   398  	})
   399  	if err != nil {
   400  		return nil, err
   401  	}
   402  	return entries, nil
   403  }
   404  
   405  // get an entity with leaf from dirID
   406  func getEntity(ctx context.Context, f *Fs, leaf string, directoryID string, token string) (*entity, error) {
   407  	var result *entity
   408  	var resultErr = fs.ErrorObjectNotFound
   409  	_, err := f.listAll(ctx, directoryID, leaf, func(entity *entity) bool {
   410  		if strings.EqualFold(entity.Name, leaf) {
   411  			// fs.Debugf(f, "getObject found entity.Name {%s} name {%s}", entity.Name, name)
   412  			if entity.isDir() {
   413  				result = nil
   414  				resultErr = fs.ErrorIsDir
   415  			} else {
   416  				result = entity
   417  				resultErr = nil
   418  			}
   419  			return true
   420  		}
   421  		return false
   422  	})
   423  	if err != nil {
   424  		return nil, err
   425  	}
   426  	return result, resultErr
   427  }
   428  
   429  // NewObject finds the Object at remote.  If it can't be found
   430  // it returns the error ErrorObjectNotFound.
   431  //
   432  // If remote points to a directory then it should return
   433  // ErrorIsDir if possible without doing any extra work,
   434  // otherwise ErrorObjectNotFound.
   435  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   436  	leaf, dirID, err := f.dirCache.FindPath(ctx, remote, false)
   437  	if err != nil {
   438  		if err == fs.ErrorDirNotFound {
   439  			return nil, fs.ErrorObjectNotFound
   440  		}
   441  		return nil, err
   442  	}
   443  
   444  	entity, err := getEntity(ctx, f, leaf, dirID, f.opt.Token)
   445  	if err != nil {
   446  		return nil, err
   447  	}
   448  	o := &Object{
   449  		fs:     f,
   450  		remote: remote,
   451  	}
   452  	o.set(entity)
   453  	return o, nil
   454  }
   455  
   456  // Mkdir makes the directory (container, bucket)
   457  //
   458  // Shouldn't return an error if it already exists
   459  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   460  	_, err := f.dirCache.FindDir(ctx, dir, true)
   461  	return err
   462  }
   463  
   464  func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
   465  	if check {
   466  		entries, err := f.List(ctx, dir)
   467  		if err != nil {
   468  			return err
   469  		}
   470  		if len(entries) != 0 {
   471  			return fs.ErrorDirectoryNotEmpty
   472  		}
   473  	}
   474  
   475  	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
   476  	if err != nil {
   477  		return err
   478  	}
   479  	opts := &rest.Opts{
   480  		Method:  "GET",
   481  		RootURL: linkboxAPIURL,
   482  		Path:    "folder_del",
   483  		Parameters: url.Values{
   484  			"token":  {f.opt.Token},
   485  			"dirIds": {directoryID},
   486  		},
   487  	}
   488  
   489  	response := response{}
   490  	err = getUnmarshaledResponse(ctx, f, opts, &response)
   491  	if err != nil {
   492  		// Linkbox has some odd error returns here
   493  		if response.Status == 403 || response.Status == 500 {
   494  			return fs.ErrorDirNotFound
   495  		}
   496  		return fmt.Errorf("purge error: %w", err)
   497  	}
   498  
   499  	f.dirCache.FlushDir(dir)
   500  	if err != nil {
   501  		return err
   502  	}
   503  	return nil
   504  }
   505  
   506  // Rmdir removes the directory (container, bucket) if empty
   507  //
   508  // Return an error if it doesn't exist or isn't empty
   509  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   510  	return f.purgeCheck(ctx, dir, true)
   511  }
   512  
   513  // SetModTime sets modTime on a particular file
   514  func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
   515  	return fs.ErrorCantSetModTime
   516  }
   517  
   518  // Open opens the file for read.  Call Close() on the returned io.ReadCloser
   519  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
   520  	var res *http.Response
   521  	downloadURL := o.fullURL
   522  	if downloadURL == "" {
   523  		_, name := splitDirAndName(o.Remote())
   524  		newObject, err := getEntity(ctx, o.fs, name, itoa64(o.dirID), o.fs.opt.Token)
   525  		if err != nil {
   526  			return nil, err
   527  		}
   528  		if newObject == nil {
   529  			// fs.Debugf(o.fs, "Open entity is empty: name = {%s}", name)
   530  			return nil, fs.ErrorObjectNotFound
   531  		}
   532  
   533  		downloadURL = newObject.URL
   534  	}
   535  
   536  	opts := &rest.Opts{
   537  		Method:  "GET",
   538  		RootURL: downloadURL,
   539  		Options: options,
   540  	}
   541  
   542  	err := o.fs.pacer.Call(func() (bool, error) {
   543  		var err error
   544  		res, err = o.fs.srv.Call(ctx, opts)
   545  		return o.fs.shouldRetry(ctx, res, err)
   546  	})
   547  
   548  	if err != nil {
   549  		return nil, fmt.Errorf("Open failed: %w", err)
   550  	}
   551  
   552  	return res.Body, nil
   553  }
   554  
   555  // Update in to the object with the modTime given of the given size
   556  //
   557  // When called from outside an Fs by rclone, src.Size() will always be >= 0.
   558  // But for unknown-sized objects (indicated by src.Size() == -1), Upload should either
   559  // return an error or update the object properly (rather than e.g. calling panic).
   560  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
   561  	size := src.Size()
   562  	if size == 0 {
   563  		return fs.ErrorCantUploadEmptyFiles
   564  	} else if size < 0 {
   565  		return fmt.Errorf("can't upload files of unknown length")
   566  	}
   567  
   568  	remote := o.Remote()
   569  
   570  	// remove the file if it exists
   571  	if o.itemID != "" {
   572  		fs.Debugf(o, "Update: removing old file")
   573  		err = o.Remove(ctx)
   574  		if err != nil {
   575  			fs.Errorf(o, "Update: failed to remove existing file: %v", err)
   576  		}
   577  		o.itemID = ""
   578  	} else {
   579  		tmpObject, err := o.fs.NewObject(ctx, remote)
   580  		if err == nil {
   581  			fs.Debugf(o, "Update: removing old file")
   582  			err = tmpObject.Remove(ctx)
   583  			if err != nil {
   584  				fs.Errorf(o, "Update: failed to remove existing file: %v", err)
   585  			}
   586  		}
   587  	}
   588  
   589  	first10m := io.LimitReader(in, 10_485_760)
   590  	first10mBytes, err := io.ReadAll(first10m)
   591  	if err != nil {
   592  		return fmt.Errorf("Update err in reading file: %w", err)
   593  	}
   594  
   595  	// get upload authorization (step 1)
   596  	opts := &rest.Opts{
   597  		Method:  "GET",
   598  		RootURL: linkboxAPIURL,
   599  		Path:    "get_upload_url",
   600  		Options: options,
   601  		Parameters: url.Values{
   602  			"token":           {o.fs.opt.Token},
   603  			"fileMd5ofPre10m": {fmt.Sprintf("%x", md5.Sum(first10mBytes))},
   604  			"fileSize":        {itoa64(size)},
   605  		},
   606  	}
   607  
   608  	getFirstStepResult := getUploadURLResponse{}
   609  	err = getUnmarshaledResponse(ctx, o.fs, opts, &getFirstStepResult)
   610  	if err != nil {
   611  		if getFirstStepResult.Status != 600 {
   612  			return fmt.Errorf("Update err in unmarshaling response: %w", err)
   613  		}
   614  	}
   615  
   616  	switch getFirstStepResult.Status {
   617  	case 1:
   618  		// upload file using link from first step
   619  		var res *http.Response
   620  
   621  		file := io.MultiReader(bytes.NewReader(first10mBytes), in)
   622  
   623  		opts := &rest.Opts{
   624  			Method:        "PUT",
   625  			RootURL:       getFirstStepResult.Data.SignURL,
   626  			Options:       options,
   627  			Body:          file,
   628  			ContentLength: &size,
   629  		}
   630  
   631  		err = o.fs.pacer.CallNoRetry(func() (bool, error) {
   632  			res, err = o.fs.srv.Call(ctx, opts)
   633  			return o.fs.shouldRetry(ctx, res, err)
   634  		})
   635  
   636  		if err != nil {
   637  			return fmt.Errorf("update err in uploading file: %w", err)
   638  		}
   639  
   640  		_, err = io.ReadAll(res.Body)
   641  		if err != nil {
   642  			return fmt.Errorf("update err in reading response: %w", err)
   643  		}
   644  
   645  	case 600:
   646  		// Status means that we don't need to upload file
   647  		// We need only to make second step
   648  	default:
   649  		return fmt.Errorf("got unexpected message from Linkbox: %s", getFirstStepResult.Message)
   650  	}
   651  
   652  	leaf, dirID, err := o.fs.dirCache.FindPath(ctx, remote, false)
   653  	if err != nil {
   654  		return err
   655  	}
   656  
   657  	// create file item at Linkbox (second step)
   658  	opts = &rest.Opts{
   659  		Method:  "GET",
   660  		RootURL: linkboxAPIURL,
   661  		Path:    "folder_upload_file",
   662  		Options: options,
   663  		Parameters: url.Values{
   664  			"token":           {o.fs.opt.Token},
   665  			"fileMd5ofPre10m": {fmt.Sprintf("%x", md5.Sum(first10mBytes))},
   666  			"fileSize":        {itoa64(size)},
   667  			"pid":             {dirID},
   668  			"diyName":         {leaf},
   669  		},
   670  	}
   671  
   672  	getSecondStepResult := getUploadURLResponse{}
   673  	err = getUnmarshaledResponse(ctx, o.fs, opts, &getSecondStepResult)
   674  	if err != nil {
   675  		return fmt.Errorf("Update second step failed: %w", err)
   676  	}
   677  
   678  	// Try a few times to read the object after upload for eventual consistency
   679  	const maxTries = 10
   680  	var sleepTime = 100 * time.Millisecond
   681  	var entity *entity
   682  	for try := 1; try <= maxTries; try++ {
   683  		entity, err = getEntity(ctx, o.fs, leaf, dirID, o.fs.opt.Token)
   684  		if err == nil {
   685  			break
   686  		}
   687  		if err != fs.ErrorObjectNotFound {
   688  			return fmt.Errorf("Update failed to read object: %w", err)
   689  		}
   690  		fs.Debugf(o, "Trying to read object after upload: try again in %v (%d/%d)", sleepTime, try, maxTries)
   691  		time.Sleep(sleepTime)
   692  		sleepTime *= 2
   693  	}
   694  	if err != nil {
   695  		return err
   696  	}
   697  	o.set(entity)
   698  	return nil
   699  }
   700  
   701  // Remove this object
   702  func (o *Object) Remove(ctx context.Context) error {
   703  	opts := &rest.Opts{
   704  		Method:  "GET",
   705  		RootURL: linkboxAPIURL,
   706  		Path:    "file_del",
   707  		Parameters: url.Values{
   708  			"token":   {o.fs.opt.Token},
   709  			"itemIds": {o.itemID},
   710  		},
   711  	}
   712  	requestResult := getUploadURLResponse{}
   713  	err := getUnmarshaledResponse(ctx, o.fs, opts, &requestResult)
   714  	if err != nil {
   715  		return fmt.Errorf("could not Remove: %w", err)
   716  
   717  	}
   718  	return nil
   719  }
   720  
   721  // ModTime returns the modification time of the remote http file
   722  func (o *Object) ModTime(ctx context.Context) time.Time {
   723  	return o.modTime
   724  }
   725  
   726  // Remote the name of the remote HTTP file, relative to the fs root
   727  func (o *Object) Remote() string {
   728  	return o.remote
   729  }
   730  
   731  // Size returns the size in bytes of the remote http file
   732  func (o *Object) Size() int64 {
   733  	return o.size
   734  }
   735  
   736  // String returns the URL to the remote HTTP file
   737  func (o *Object) String() string {
   738  	if o == nil {
   739  		return "<nil>"
   740  	}
   741  	return o.remote
   742  }
   743  
   744  // Fs is the filesystem this remote http file object is located within
   745  func (o *Object) Fs() fs.Info {
   746  	return o.fs
   747  }
   748  
   749  // Hash returns "" since HTTP (in Go or OpenSSH) doesn't support remote calculation of hashes
   750  func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
   751  	return "", hash.ErrUnsupported
   752  }
   753  
   754  // Storable returns whether the remote http file is a regular file
   755  // (not a directory, symbolic link, block device, character device, named pipe, etc.)
   756  func (o *Object) Storable() bool {
   757  	return true
   758  }
   759  
   760  // Features returns the optional features of this Fs
   761  // Info provides a read only interface to information about a filesystem.
   762  func (f *Fs) Features() *fs.Features {
   763  	return f.features
   764  }
   765  
   766  // Name of the remote (as passed into NewFs)
   767  // Name returns the configured name of the file system
   768  func (f *Fs) Name() string {
   769  	return f.name
   770  }
   771  
   772  // Root of the remote (as passed into NewFs)
   773  func (f *Fs) Root() string {
   774  	return f.root
   775  }
   776  
   777  // String returns a description of the FS
   778  func (f *Fs) String() string {
   779  	return fmt.Sprintf("Linkbox root '%s'", f.root)
   780  }
   781  
   782  // Precision of the ModTimes in this Fs
   783  func (f *Fs) Precision() time.Duration {
   784  	return fs.ModTimeNotSupported
   785  }
   786  
   787  // Hashes returns hash.HashNone to indicate remote hashing is unavailable
   788  // Returns the supported hash types of the filesystem
   789  func (f *Fs) Hashes() hash.Set {
   790  	return hash.Set(hash.None)
   791  }
   792  
   793  /*
   794  	{
   795  	  "data": {
   796  	    "signUrl": "http://xx -- Then CURL PUT your file with sign url "
   797  	  },
   798  	  "msg": "please use this url to upload (PUT method)",
   799  	  "status": 1
   800  	}
   801  */
   802  
   803  // All messages have these items
   804  type response struct {
   805  	Message string `json:"msg"`
   806  	Status  int    `json:"status"`
   807  }
   808  
   809  // IsError returns whether response represents an error
   810  func (r *response) IsError() bool {
   811  	return r.Status != 1
   812  }
   813  
   814  // Error returns the error state of this response
   815  func (r *response) Error() string {
   816  	return fmt.Sprintf("Linkbox error %d: %s", r.Status, r.Message)
   817  }
   818  
   819  // responser is interface covering the response so we can use it when it is embedded.
   820  type responser interface {
   821  	IsError() bool
   822  	Error() string
   823  }
   824  
   825  type getUploadURLData struct {
   826  	SignURL string `json:"signUrl"`
   827  }
   828  
   829  type getUploadURLResponse struct {
   830  	response
   831  	Data getUploadURLData `json:"data"`
   832  }
   833  
   834  // Put in to the remote path with the modTime given of the given size
   835  //
   836  // When called from outside an Fs by rclone, src.Size() will always be >= 0.
   837  // But for unknown-sized objects (indicated by src.Size() == -1), Put should either
   838  // return an error or upload it properly (rather than e.g. calling panic).
   839  //
   840  // May create the object even if it returns an error - if so
   841  // will return the object and the error, otherwise will return
   842  // nil and the error
   843  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   844  	o := &Object{
   845  		fs:     f,
   846  		remote: src.Remote(),
   847  		size:   src.Size(),
   848  	}
   849  	dir, _ := splitDirAndName(src.Remote())
   850  	err := f.Mkdir(ctx, dir)
   851  	if err != nil {
   852  		return nil, err
   853  	}
   854  	err = o.Update(ctx, in, src, options...)
   855  	return o, err
   856  }
   857  
   858  // Purge all files in the directory specified
   859  //
   860  // Implement this if you have a way of deleting all the files
   861  // quicker than just running Remove() on the result of List()
   862  //
   863  // Return an error if it doesn't exist
   864  func (f *Fs) Purge(ctx context.Context, dir string) error {
   865  	return f.purgeCheck(ctx, dir, false)
   866  }
   867  
   868  // retryErrorCodes is a slice of error codes that we will retry
   869  var retryErrorCodes = []int{
   870  	429, // Too Many Requests.
   871  	500, // Internal Server Error
   872  	502, // Bad Gateway
   873  	503, // Service Unavailable
   874  	504, // Gateway Timeout
   875  	509, // Bandwidth Limit Exceeded
   876  }
   877  
   878  // shouldRetry determines whether a given err rates being retried
   879  func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
   880  	if fserrors.ContextError(ctx, &err) {
   881  		return false, err
   882  	}
   883  	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
   884  }
   885  
   886  // DirCacheFlush resets the directory cache - used in testing as an
   887  // optional interface
   888  func (f *Fs) DirCacheFlush() {
   889  	f.dirCache.ResetRoot()
   890  }
   891  
   892  // Check the interfaces are satisfied
   893  var (
   894  	_ fs.Fs              = &Fs{}
   895  	_ fs.Purger          = &Fs{}
   896  	_ fs.DirCacheFlusher = &Fs{}
   897  	_ fs.Object          = &Object{}
   898  )