github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/jottacloud/jottacloud.go (about)

     1  package jottacloud
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/md5"
     7  	"encoding/base64"
     8  	"encoding/hex"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"log"
    14  	"net/http"
    15  	"net/url"
    16  	"os"
    17  	"path"
    18  	"strconv"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/pkg/errors"
    23  	"github.com/rclone/rclone/backend/jottacloud/api"
    24  	"github.com/rclone/rclone/fs"
    25  	"github.com/rclone/rclone/fs/accounting"
    26  	"github.com/rclone/rclone/fs/config"
    27  	"github.com/rclone/rclone/fs/config/configmap"
    28  	"github.com/rclone/rclone/fs/config/configstruct"
    29  	"github.com/rclone/rclone/fs/fserrors"
    30  	"github.com/rclone/rclone/fs/fshttp"
    31  	"github.com/rclone/rclone/fs/hash"
    32  	"github.com/rclone/rclone/fs/walk"
    33  	"github.com/rclone/rclone/lib/encoder"
    34  	"github.com/rclone/rclone/lib/oauthutil"
    35  	"github.com/rclone/rclone/lib/pacer"
    36  	"github.com/rclone/rclone/lib/rest"
    37  	"golang.org/x/oauth2"
    38  )
    39  
    40  // Globals
    41  const (
    42  	minSleep          = 10 * time.Millisecond
    43  	maxSleep          = 2 * time.Second
    44  	decayConstant     = 2 // bigger for slower decay, exponential
    45  	defaultDevice     = "Jotta"
    46  	defaultMountpoint = "Archive"
    47  	rootURL           = "https://www.jottacloud.com/jfs/"
    48  	apiURL            = "https://api.jottacloud.com/"
    49  	baseURL           = "https://www.jottacloud.com/"
    50  	defaultTokenURL   = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token"
    51  	cachePrefix       = "rclone-jcmd5-"
    52  	configDevice      = "device"
    53  	configMountpoint  = "mountpoint"
    54  	configTokenURL    = "tokenURL"
    55  	configVersion     = 1
    56  )
    57  
    58  var (
    59  	// Description of how to auth for this app for a personal account
    60  	oauthConfig = &oauth2.Config{
    61  		ClientID: "jottacli",
    62  		Endpoint: oauth2.Endpoint{
    63  			AuthURL:  defaultTokenURL,
    64  			TokenURL: defaultTokenURL,
    65  		},
    66  		RedirectURL: oauthutil.RedirectLocalhostURL,
    67  	}
    68  )
    69  
    70  // Register with Fs
    71  func init() {
    72  	// needs to be done early so we can use oauth during config
    73  	fs.Register(&fs.RegInfo{
    74  		Name:        "jottacloud",
    75  		Description: "Jottacloud",
    76  		NewFs:       NewFs,
    77  		Config: func(name string, m configmap.Mapper) {
    78  			ctx := context.TODO()
    79  
    80  			refresh := false
    81  			if version, ok := m.Get("configVersion"); ok {
    82  				ver, err := strconv.Atoi(version)
    83  				if err != nil {
    84  					log.Fatalf("Failed to parse config version - corrupted config")
    85  				}
    86  				refresh = ver != configVersion
    87  			}
    88  
    89  			if refresh {
    90  				fmt.Printf("Config outdated - refreshing\n")
    91  			} else {
    92  				tokenString, ok := m.Get("token")
    93  				if ok && tokenString != "" {
    94  					fmt.Printf("Already have a token - refresh?\n")
    95  					if !config.Confirm(false) {
    96  						return
    97  					}
    98  				}
    99  			}
   100  
   101  			clientConfig := *fs.Config
   102  			clientConfig.UserAgent = "JottaCli 0.6.18626 windows-amd64"
   103  			srv := rest.NewClient(fshttp.NewClient(&clientConfig))
   104  
   105  			fmt.Printf("Generate a personal login token here: https://www.jottacloud.com/web/secure\n")
   106  			fmt.Printf("Login Token> ")
   107  			loginToken := config.ReadLine()
   108  
   109  			token, err := doAuth(ctx, srv, loginToken, m)
   110  			if err != nil {
   111  				log.Fatalf("Failed to get oauth token: %s", err)
   112  			}
   113  			err = oauthutil.PutToken(name, m, &token, true)
   114  			if err != nil {
   115  				log.Fatalf("Error while saving token: %s", err)
   116  			}
   117  
   118  			fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n")
   119  			if config.Confirm(false) {
   120  				oAuthClient, _, err := oauthutil.NewClient(name, m, oauthConfig)
   121  				if err != nil {
   122  					log.Fatalf("Failed to load oAuthClient: %s", err)
   123  				}
   124  
   125  				srv = rest.NewClient(oAuthClient).SetRoot(rootURL)
   126  				apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL)
   127  
   128  				device, mountpoint, err := setupMountpoint(ctx, srv, apiSrv)
   129  				if err != nil {
   130  					log.Fatalf("Failed to setup mountpoint: %s", err)
   131  				}
   132  				m.Set(configDevice, device)
   133  				m.Set(configMountpoint, mountpoint)
   134  			}
   135  
   136  			m.Set("configVersion", strconv.Itoa(configVersion))
   137  		},
   138  		Options: []fs.Option{{
   139  			Name:     "md5_memory_limit",
   140  			Help:     "Files bigger than this will be cached on disk to calculate the MD5 if required.",
   141  			Default:  fs.SizeSuffix(10 * 1024 * 1024),
   142  			Advanced: true,
   143  		}, {
   144  			Name:     "trashed_only",
   145  			Help:     "Only show files that are in the trash.\nThis will show trashed files in their original directory structure.",
   146  			Default:  false,
   147  			Advanced: true,
   148  		}, {
   149  			Name:     "hard_delete",
   150  			Help:     "Delete files permanently rather than putting them into the trash.",
   151  			Default:  false,
   152  			Advanced: true,
   153  		}, {
   154  			Name:     "unlink",
   155  			Help:     "Remove existing public link to file/folder with link command rather than creating.\nDefault is false, meaning link command will create or retrieve public link.",
   156  			Default:  false,
   157  			Advanced: true,
   158  		}, {
   159  			Name:     "upload_resume_limit",
   160  			Help:     "Files bigger than this can be resumed if the upload fail's.",
   161  			Default:  fs.SizeSuffix(10 * 1024 * 1024),
   162  			Advanced: true,
   163  		}, {
   164  			Name:     config.ConfigEncoding,
   165  			Help:     config.ConfigEncodingHelp,
   166  			Advanced: true,
   167  			// Encode invalid UTF-8 bytes as xml doesn't handle them properly.
   168  			//
   169  			// Also: '*', '/', ':', '<', '>', '?', '\"', '\x00', '|'
   170  			Default: (encoder.Display |
   171  				encoder.EncodeWin | // :?"*<>|
   172  				encoder.EncodeInvalidUtf8),
   173  		}},
   174  	})
   175  }
   176  
   177  // Options defines the configuration for this backend
   178  type Options struct {
   179  	Device             string               `config:"device"`
   180  	Mountpoint         string               `config:"mountpoint"`
   181  	MD5MemoryThreshold fs.SizeSuffix        `config:"md5_memory_limit"`
   182  	TrashedOnly        bool                 `config:"trashed_only"`
   183  	HardDelete         bool                 `config:"hard_delete"`
   184  	Unlink             bool                 `config:"unlink"`
   185  	UploadThreshold    fs.SizeSuffix        `config:"upload_resume_limit"`
   186  	Enc                encoder.MultiEncoder `config:"encoding"`
   187  }
   188  
   189  // Fs represents a remote jottacloud
   190  type Fs struct {
   191  	name         string
   192  	root         string
   193  	user         string
   194  	opt          Options
   195  	features     *fs.Features
   196  	endpointURL  string
   197  	srv          *rest.Client
   198  	apiSrv       *rest.Client
   199  	pacer        *fs.Pacer
   200  	tokenRenewer *oauthutil.Renew // renew the token on expiry
   201  }
   202  
   203  // Object describes a jottacloud object
   204  //
   205  // Will definitely have info but maybe not meta
   206  type Object struct {
   207  	fs          *Fs
   208  	remote      string
   209  	hasMetaData bool
   210  	size        int64
   211  	modTime     time.Time
   212  	md5         string
   213  	mimeType    string
   214  }
   215  
   216  // ------------------------------------------------------------
   217  
   218  // Name of the remote (as passed into NewFs)
   219  func (f *Fs) Name() string {
   220  	return f.name
   221  }
   222  
   223  // Root of the remote (as passed into NewFs)
   224  func (f *Fs) Root() string {
   225  	return f.root
   226  }
   227  
   228  // String converts this Fs to a string
   229  func (f *Fs) String() string {
   230  	return fmt.Sprintf("jottacloud root '%s'", f.root)
   231  }
   232  
   233  // Features returns the optional features of this Fs
   234  func (f *Fs) Features() *fs.Features {
   235  	return f.features
   236  }
   237  
   238  // parsePath parses a box 'url'
   239  func parsePath(path string) (root string) {
   240  	root = strings.Trim(path, "/")
   241  	return
   242  }
   243  
   244  // retryErrorCodes is a slice of error codes that we will retry
   245  var retryErrorCodes = []int{
   246  	429, // Too Many Requests.
   247  	500, // Internal Server Error
   248  	502, // Bad Gateway
   249  	503, // Service Unavailable
   250  	504, // Gateway Timeout
   251  	509, // Bandwidth Limit Exceeded
   252  }
   253  
   254  // shouldRetry returns a boolean as to whether this resp and err
   255  // deserve to be retried.  It returns the err as a convenience
   256  func shouldRetry(resp *http.Response, err error) (bool, error) {
   257  	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
   258  }
   259  
   260  // doAuth runs the actual token request
   261  func doAuth(ctx context.Context, srv *rest.Client, loginTokenBase64 string, m configmap.Mapper) (token oauth2.Token, err error) {
   262  	loginTokenBytes, err := base64.RawURLEncoding.DecodeString(loginTokenBase64)
   263  	if err != nil {
   264  		return token, err
   265  	}
   266  
   267  	// decode login token
   268  	var loginToken api.LoginToken
   269  	decoder := json.NewDecoder(bytes.NewReader(loginTokenBytes))
   270  	err = decoder.Decode(&loginToken)
   271  	if err != nil {
   272  		return token, err
   273  	}
   274  
   275  	// retrieve endpoint urls
   276  	opts := rest.Opts{
   277  		Method:  "GET",
   278  		RootURL: loginToken.WellKnownLink,
   279  	}
   280  	var wellKnown api.WellKnown
   281  	_, err = srv.CallJSON(ctx, &opts, nil, &wellKnown)
   282  	if err != nil {
   283  		return token, err
   284  	}
   285  
   286  	// save the tokenurl
   287  	oauthConfig.Endpoint.AuthURL = wellKnown.TokenEndpoint
   288  	oauthConfig.Endpoint.TokenURL = wellKnown.TokenEndpoint
   289  	m.Set(configTokenURL, wellKnown.TokenEndpoint)
   290  
   291  	// prepare out token request with username and password
   292  	values := url.Values{}
   293  	values.Set("client_id", "jottacli")
   294  	values.Set("grant_type", "password")
   295  	values.Set("password", loginToken.AuthToken)
   296  	values.Set("scope", "offline_access+openid")
   297  	values.Set("username", loginToken.Username)
   298  	values.Encode()
   299  	opts = rest.Opts{
   300  		Method:      "POST",
   301  		RootURL:     oauthConfig.Endpoint.AuthURL,
   302  		ContentType: "application/x-www-form-urlencoded",
   303  		Body:        strings.NewReader(values.Encode()),
   304  	}
   305  
   306  	// do the first request
   307  	var jsonToken api.TokenJSON
   308  	_, err = srv.CallJSON(ctx, &opts, nil, &jsonToken)
   309  	if err != nil {
   310  		return token, err
   311  	}
   312  
   313  	token.AccessToken = jsonToken.AccessToken
   314  	token.RefreshToken = jsonToken.RefreshToken
   315  	token.TokenType = jsonToken.TokenType
   316  	token.Expiry = time.Now().Add(time.Duration(jsonToken.ExpiresIn) * time.Second)
   317  	return token, err
   318  }
   319  
   320  // setupMountpoint sets up a custom device and mountpoint if desired by the user
   321  func setupMountpoint(ctx context.Context, srv *rest.Client, apiSrv *rest.Client) (device, mountpoint string, err error) {
   322  	cust, err := getCustomerInfo(ctx, apiSrv)
   323  	if err != nil {
   324  		return "", "", err
   325  	}
   326  
   327  	acc, err := getDriveInfo(ctx, srv, cust.Username)
   328  	if err != nil {
   329  		return "", "", err
   330  	}
   331  	var deviceNames []string
   332  	for i := range acc.Devices {
   333  		deviceNames = append(deviceNames, acc.Devices[i].Name)
   334  	}
   335  	fmt.Printf("Please select the device to use. Normally this will be Jotta\n")
   336  	device = config.Choose("Devices", deviceNames, nil, false)
   337  
   338  	dev, err := getDeviceInfo(ctx, srv, path.Join(cust.Username, device))
   339  	if err != nil {
   340  		return "", "", err
   341  	}
   342  	if len(dev.MountPoints) == 0 {
   343  		return "", "", errors.New("no mountpoints for selected device")
   344  	}
   345  	var mountpointNames []string
   346  	for i := range dev.MountPoints {
   347  		mountpointNames = append(mountpointNames, dev.MountPoints[i].Name)
   348  	}
   349  	fmt.Printf("Please select the mountpoint to user. Normally this will be Archive\n")
   350  	mountpoint = config.Choose("Mountpoints", mountpointNames, nil, false)
   351  
   352  	return device, mountpoint, err
   353  }
   354  
   355  // getCustomerInfo queries general information about the account
   356  func getCustomerInfo(ctx context.Context, srv *rest.Client) (info *api.CustomerInfo, err error) {
   357  	opts := rest.Opts{
   358  		Method: "GET",
   359  		Path:   "account/v1/customer",
   360  	}
   361  
   362  	_, err = srv.CallJSON(ctx, &opts, nil, &info)
   363  	if err != nil {
   364  		return nil, errors.Wrap(err, "couldn't get customer info")
   365  	}
   366  
   367  	return info, nil
   368  }
   369  
   370  // getDriveInfo queries general information about the account and the available devices and mountpoints.
   371  func getDriveInfo(ctx context.Context, srv *rest.Client, username string) (info *api.DriveInfo, err error) {
   372  	opts := rest.Opts{
   373  		Method: "GET",
   374  		Path:   username,
   375  	}
   376  
   377  	_, err = srv.CallXML(ctx, &opts, nil, &info)
   378  	if err != nil {
   379  		return nil, errors.Wrap(err, "couldn't get drive info")
   380  	}
   381  
   382  	return info, nil
   383  }
   384  
   385  // getDeviceInfo queries Information about a jottacloud device
   386  func getDeviceInfo(ctx context.Context, srv *rest.Client, path string) (info *api.JottaDevice, err error) {
   387  	opts := rest.Opts{
   388  		Method: "GET",
   389  		Path:   urlPathEscape(path),
   390  	}
   391  
   392  	_, err = srv.CallXML(ctx, &opts, nil, &info)
   393  	if err != nil {
   394  		return nil, errors.Wrap(err, "couldn't get device info")
   395  	}
   396  
   397  	return info, nil
   398  }
   399  
   400  // setEndpointURL generates the API endpoint URL
   401  func (f *Fs) setEndpointURL() {
   402  	if f.opt.Device == "" {
   403  		f.opt.Device = defaultDevice
   404  	}
   405  	if f.opt.Mountpoint == "" {
   406  		f.opt.Mountpoint = defaultMountpoint
   407  	}
   408  	f.endpointURL = urlPathEscape(path.Join(f.user, f.opt.Device, f.opt.Mountpoint))
   409  }
   410  
   411  // readMetaDataForPath reads the metadata from the path
   412  func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.JottaFile, err error) {
   413  	opts := rest.Opts{
   414  		Method: "GET",
   415  		Path:   f.filePath(path),
   416  	}
   417  	var result api.JottaFile
   418  	var resp *http.Response
   419  	err = f.pacer.Call(func() (bool, error) {
   420  		resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
   421  		return shouldRetry(resp, err)
   422  	})
   423  
   424  	if apiErr, ok := err.(*api.Error); ok {
   425  		// does not exist
   426  		if apiErr.StatusCode == http.StatusNotFound {
   427  			return nil, fs.ErrorObjectNotFound
   428  		}
   429  	}
   430  
   431  	if err != nil {
   432  		return nil, errors.Wrap(err, "read metadata failed")
   433  	}
   434  	if result.XMLName.Local != "file" {
   435  		return nil, fs.ErrorNotAFile
   436  	}
   437  	return &result, nil
   438  }
   439  
   440  // errorHandler parses a non 2xx error response into an error
   441  func errorHandler(resp *http.Response) error {
   442  	// Decode error response
   443  	errResponse := new(api.Error)
   444  	err := rest.DecodeXML(resp, &errResponse)
   445  	if err != nil {
   446  		fs.Debugf(nil, "Couldn't decode error response: %v", err)
   447  	}
   448  	if errResponse.Message == "" {
   449  		errResponse.Message = resp.Status
   450  	}
   451  	if errResponse.StatusCode == 0 {
   452  		errResponse.StatusCode = resp.StatusCode
   453  	}
   454  	return errResponse
   455  }
   456  
   457  // Jottacloud wants '+' to be URL encoded even though the RFC states it's not reserved
   458  func urlPathEscape(in string) string {
   459  	return strings.Replace(rest.URLPathEscape(in), "+", "%2B", -1)
   460  }
   461  
   462  // filePathRaw returns an unescaped file path (f.root, file)
   463  func (f *Fs) filePathRaw(file string) string {
   464  	return path.Join(f.endpointURL, f.opt.Enc.FromStandardPath(path.Join(f.root, file)))
   465  }
   466  
   467  // filePath returns an escaped file path (f.root, file)
   468  func (f *Fs) filePath(file string) string {
   469  	return urlPathEscape(f.filePathRaw(file))
   470  }
   471  
   472  // NewFs constructs an Fs from the path, container:path
   473  func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
   474  	ctx := context.TODO()
   475  	// Parse config into Options struct
   476  	opt := new(Options)
   477  	err := configstruct.Set(m, opt)
   478  	if err != nil {
   479  		return nil, err
   480  	}
   481  
   482  	// Check config version
   483  	var ok bool
   484  	var version string
   485  	if version, ok = m.Get("configVersion"); ok {
   486  		ver, err := strconv.Atoi(version)
   487  		if err != nil {
   488  			return nil, errors.New("Failed to parse config version")
   489  		}
   490  		ok = ver == configVersion
   491  	}
   492  	if !ok {
   493  		return nil, errors.New("Outdated config - please reconfigure this backend")
   494  	}
   495  
   496  	// if custom endpoints are set use them else stick with defaults
   497  	if tokenURL, ok := m.Get(configTokenURL); ok {
   498  		oauthConfig.Endpoint.TokenURL = tokenURL
   499  		// jottacloud is weird. we need to use the tokenURL as authURL
   500  		oauthConfig.Endpoint.AuthURL = tokenURL
   501  	}
   502  
   503  	// Create OAuth Client
   504  	baseClient := fshttp.NewClient(fs.Config)
   505  	oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(name, m, oauthConfig, baseClient)
   506  	if err != nil {
   507  		return nil, errors.Wrap(err, "Failed to configure Jottacloud oauth client")
   508  	}
   509  
   510  	rootIsDir := strings.HasSuffix(root, "/")
   511  	root = parsePath(root)
   512  
   513  	f := &Fs{
   514  		name:   name,
   515  		root:   root,
   516  		opt:    *opt,
   517  		srv:    rest.NewClient(oAuthClient).SetRoot(rootURL),
   518  		apiSrv: rest.NewClient(oAuthClient).SetRoot(apiURL),
   519  		pacer:  fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
   520  	}
   521  	f.features = (&fs.Features{
   522  		CaseInsensitive:         true,
   523  		CanHaveEmptyDirectories: true,
   524  		ReadMimeType:            true,
   525  		WriteMimeType:           true,
   526  	}).Fill(f)
   527  	f.srv.SetErrorHandler(errorHandler)
   528  	if opt.TrashedOnly { // we cannot support showing Trashed Files when using ListR right now
   529  		f.features.ListR = nil
   530  	}
   531  
   532  	// Renew the token in the background
   533  	f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error {
   534  		_, err := f.readMetaDataForPath(ctx, "")
   535  		return err
   536  	})
   537  
   538  	cust, err := getCustomerInfo(ctx, f.apiSrv)
   539  	if err != nil {
   540  		return nil, err
   541  	}
   542  	f.user = cust.Username
   543  	f.setEndpointURL()
   544  
   545  	if root != "" && !rootIsDir {
   546  		// Check to see if the root actually an existing file
   547  		remote := path.Base(root)
   548  		f.root = path.Dir(root)
   549  		if f.root == "." {
   550  			f.root = ""
   551  		}
   552  		_, err := f.NewObject(context.TODO(), remote)
   553  		if err != nil {
   554  			if errors.Cause(err) == fs.ErrorObjectNotFound || errors.Cause(err) == fs.ErrorNotAFile {
   555  				// File doesn't exist so return old f
   556  				f.root = root
   557  				return f, nil
   558  			}
   559  			return nil, err
   560  		}
   561  		// return an error with an fs which points to the parent
   562  		return f, fs.ErrorIsFile
   563  	}
   564  	return f, nil
   565  }
   566  
   567  // Return an Object from a path
   568  //
   569  // If it can't be found it returns the error fs.ErrorObjectNotFound.
   570  func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.JottaFile) (fs.Object, error) {
   571  	o := &Object{
   572  		fs:     f,
   573  		remote: remote,
   574  	}
   575  	var err error
   576  	if info != nil {
   577  		// Set info
   578  		err = o.setMetaData(info)
   579  	} else {
   580  		err = o.readMetaData(ctx, false) // reads info and meta, returning an error
   581  	}
   582  	if err != nil {
   583  		return nil, err
   584  	}
   585  	return o, nil
   586  }
   587  
   588  // NewObject finds the Object at remote.  If it can't be found
   589  // it returns the error fs.ErrorObjectNotFound.
   590  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   591  	return f.newObjectWithInfo(ctx, remote, nil)
   592  }
   593  
   594  // CreateDir makes a directory
   595  func (f *Fs) CreateDir(ctx context.Context, path string) (jf *api.JottaFolder, err error) {
   596  	// fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, leaf)
   597  	var resp *http.Response
   598  	opts := rest.Opts{
   599  		Method:     "POST",
   600  		Path:       f.filePath(path),
   601  		Parameters: url.Values{},
   602  	}
   603  
   604  	opts.Parameters.Set("mkDir", "true")
   605  
   606  	err = f.pacer.Call(func() (bool, error) {
   607  		resp, err = f.srv.CallXML(ctx, &opts, nil, &jf)
   608  		return shouldRetry(resp, err)
   609  	})
   610  	if err != nil {
   611  		//fmt.Printf("...Error %v\n", err)
   612  		return nil, err
   613  	}
   614  	// fmt.Printf("...Id %q\n", *info.Id)
   615  	return jf, nil
   616  }
   617  
   618  // List the objects and directories in dir into entries.  The
   619  // entries can be returned in any order but should be for a
   620  // complete directory.
   621  //
   622  // dir should be "" to list the root, and should not have
   623  // trailing slashes.
   624  //
   625  // This should return ErrDirNotFound if the directory isn't
   626  // found.
   627  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   628  	opts := rest.Opts{
   629  		Method: "GET",
   630  		Path:   f.filePath(dir),
   631  	}
   632  
   633  	var resp *http.Response
   634  	var result api.JottaFolder
   635  	err = f.pacer.Call(func() (bool, error) {
   636  		resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
   637  		return shouldRetry(resp, err)
   638  	})
   639  
   640  	if err != nil {
   641  		if apiErr, ok := err.(*api.Error); ok {
   642  			// does not exist
   643  			if apiErr.StatusCode == http.StatusNotFound {
   644  				return nil, fs.ErrorDirNotFound
   645  			}
   646  		}
   647  		return nil, errors.Wrap(err, "couldn't list files")
   648  	}
   649  
   650  	if bool(result.Deleted) && !f.opt.TrashedOnly {
   651  		return nil, fs.ErrorDirNotFound
   652  	}
   653  
   654  	for i := range result.Folders {
   655  		item := &result.Folders[i]
   656  		if !f.opt.TrashedOnly && bool(item.Deleted) {
   657  			continue
   658  		}
   659  		remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name))
   660  		d := fs.NewDir(remote, time.Time(item.ModifiedAt))
   661  		entries = append(entries, d)
   662  	}
   663  
   664  	for i := range result.Files {
   665  		item := &result.Files[i]
   666  		if f.opt.TrashedOnly {
   667  			if !item.Deleted || item.State != "COMPLETED" {
   668  				continue
   669  			}
   670  		} else {
   671  			if item.Deleted || item.State != "COMPLETED" {
   672  				continue
   673  			}
   674  		}
   675  		remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name))
   676  		o, err := f.newObjectWithInfo(ctx, remote, item)
   677  		if err != nil {
   678  			continue
   679  		}
   680  		entries = append(entries, o)
   681  	}
   682  	return entries, nil
   683  }
   684  
   685  // listFileDirFn is called from listFileDir to handle an object.
   686  type listFileDirFn func(fs.DirEntry) error
   687  
   688  // List the objects and directories into entries, from a
   689  // special kind of JottaFolder representing a FileDirLis
   690  func (f *Fs) listFileDir(ctx context.Context, remoteStartPath string, startFolder *api.JottaFolder, fn listFileDirFn) error {
   691  	pathPrefix := "/" + f.filePathRaw("") // Non-escaped prefix of API paths to be cut off, to be left with the remote path including the remoteStartPath
   692  	pathPrefixLength := len(pathPrefix)
   693  	startPath := path.Join(pathPrefix, remoteStartPath) // Non-escaped API path up to and including remoteStartPath, to decide if it should be created as a new dir object
   694  	startPathLength := len(startPath)
   695  	for i := range startFolder.Folders {
   696  		folder := &startFolder.Folders[i]
   697  		if folder.Deleted {
   698  			return nil
   699  		}
   700  		folderPath := f.opt.Enc.ToStandardPath(path.Join(folder.Path, folder.Name))
   701  		folderPathLength := len(folderPath)
   702  		var remoteDir string
   703  		if folderPathLength > pathPrefixLength {
   704  			remoteDir = folderPath[pathPrefixLength+1:]
   705  			if folderPathLength > startPathLength {
   706  				d := fs.NewDir(remoteDir, time.Time(folder.ModifiedAt))
   707  				err := fn(d)
   708  				if err != nil {
   709  					return err
   710  				}
   711  			}
   712  		}
   713  		for i := range folder.Files {
   714  			file := &folder.Files[i]
   715  			if file.Deleted || file.State != "COMPLETED" {
   716  				continue
   717  			}
   718  			remoteFile := path.Join(remoteDir, f.opt.Enc.ToStandardName(file.Name))
   719  			o, err := f.newObjectWithInfo(ctx, remoteFile, file)
   720  			if err != nil {
   721  				return err
   722  			}
   723  			err = fn(o)
   724  			if err != nil {
   725  				return err
   726  			}
   727  		}
   728  	}
   729  	return nil
   730  }
   731  
   732  // ListR lists the objects and directories of the Fs starting
   733  // from dir recursively into out.
   734  //
   735  // dir should be "" to start from the root, and should not
   736  // have trailing slashes.
   737  func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) {
   738  	opts := rest.Opts{
   739  		Method:     "GET",
   740  		Path:       f.filePath(dir),
   741  		Parameters: url.Values{},
   742  	}
   743  	opts.Parameters.Set("mode", "list")
   744  
   745  	var resp *http.Response
   746  	var result api.JottaFolder // Could be JottaFileDirList, but JottaFolder is close enough
   747  	err = f.pacer.Call(func() (bool, error) {
   748  		resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
   749  		return shouldRetry(resp, err)
   750  	})
   751  	if err != nil {
   752  		if apiErr, ok := err.(*api.Error); ok {
   753  			// does not exist
   754  			if apiErr.StatusCode == http.StatusNotFound {
   755  				return fs.ErrorDirNotFound
   756  			}
   757  		}
   758  		return errors.Wrap(err, "couldn't list files")
   759  	}
   760  	list := walk.NewListRHelper(callback)
   761  	err = f.listFileDir(ctx, dir, &result, func(entry fs.DirEntry) error {
   762  		return list.Add(entry)
   763  	})
   764  	if err != nil {
   765  		return err
   766  	}
   767  	return list.Flush()
   768  }
   769  
   770  // Creates from the parameters passed in a half finished Object which
   771  // must have setMetaData called on it
   772  //
   773  // Used to create new objects
   774  func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Object) {
   775  	// Temporary Object under construction
   776  	o = &Object{
   777  		fs:      f,
   778  		remote:  remote,
   779  		size:    size,
   780  		modTime: modTime,
   781  	}
   782  	return o
   783  }
   784  
   785  // Put the object
   786  //
   787  // Copy the reader in to the new object which is returned
   788  //
   789  // The new object may have been created if an error is returned
   790  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   791  	if f.opt.Device != "Jotta" {
   792  		return nil, errors.New("upload not supported for devices other than Jotta")
   793  	}
   794  	o := f.createObject(src.Remote(), src.ModTime(ctx), src.Size())
   795  	return o, o.Update(ctx, in, src, options...)
   796  }
   797  
   798  // mkParentDir makes the parent of the native path dirPath if
   799  // necessary and any directories above that
   800  func (f *Fs) mkParentDir(ctx context.Context, dirPath string) error {
   801  	// defer log.Trace(dirPath, "")("")
   802  	// chop off trailing / if it exists
   803  	if strings.HasSuffix(dirPath, "/") {
   804  		dirPath = dirPath[:len(dirPath)-1]
   805  	}
   806  	parent := path.Dir(dirPath)
   807  	if parent == "." {
   808  		parent = ""
   809  	}
   810  	return f.Mkdir(ctx, parent)
   811  }
   812  
   813  // Mkdir creates the container if it doesn't exist
   814  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   815  	_, err := f.CreateDir(ctx, dir)
   816  	return err
   817  }
   818  
   819  // purgeCheck removes the root directory, if check is set then it
   820  // refuses to do so if it has anything in
   821  func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) (err error) {
   822  	root := path.Join(f.root, dir)
   823  	if root == "" {
   824  		return errors.New("can't purge root directory")
   825  	}
   826  
   827  	// check that the directory exists
   828  	entries, err := f.List(ctx, dir)
   829  	if err != nil {
   830  		return err
   831  	}
   832  
   833  	if check {
   834  		if len(entries) != 0 {
   835  			return fs.ErrorDirectoryNotEmpty
   836  		}
   837  	}
   838  
   839  	opts := rest.Opts{
   840  		Method:     "POST",
   841  		Path:       f.filePath(dir),
   842  		Parameters: url.Values{},
   843  		NoResponse: true,
   844  	}
   845  
   846  	if f.opt.HardDelete {
   847  		opts.Parameters.Set("rmDir", "true")
   848  	} else {
   849  		opts.Parameters.Set("dlDir", "true")
   850  	}
   851  
   852  	var resp *http.Response
   853  	err = f.pacer.Call(func() (bool, error) {
   854  		resp, err = f.srv.Call(ctx, &opts)
   855  		return shouldRetry(resp, err)
   856  	})
   857  	if err != nil {
   858  		return errors.Wrap(err, "couldn't purge directory")
   859  	}
   860  
   861  	return nil
   862  }
   863  
   864  // Rmdir deletes the root folder
   865  //
   866  // Returns an error if it isn't empty
   867  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   868  	return f.purgeCheck(ctx, dir, true)
   869  }
   870  
   871  // Precision return the precision of this Fs
   872  func (f *Fs) Precision() time.Duration {
   873  	return time.Second
   874  }
   875  
   876  // Purge deletes all the files and the container
   877  func (f *Fs) Purge(ctx context.Context) error {
   878  	return f.purgeCheck(ctx, "", false)
   879  }
   880  
   881  // copyOrMoves copies or moves directories or files depending on the method parameter
   882  func (f *Fs) copyOrMove(ctx context.Context, method, src, dest string) (info *api.JottaFile, err error) {
   883  	opts := rest.Opts{
   884  		Method:     "POST",
   885  		Path:       src,
   886  		Parameters: url.Values{},
   887  	}
   888  
   889  	opts.Parameters.Set(method, "/"+path.Join(f.endpointURL, f.opt.Enc.FromStandardPath(path.Join(f.root, dest))))
   890  
   891  	var resp *http.Response
   892  	err = f.pacer.Call(func() (bool, error) {
   893  		resp, err = f.srv.CallXML(ctx, &opts, nil, &info)
   894  		return shouldRetry(resp, err)
   895  	})
   896  	if err != nil {
   897  		return nil, err
   898  	}
   899  	return info, nil
   900  }
   901  
   902  // Copy src to this remote using server side copy operations.
   903  //
   904  // This is stored with the remote path given
   905  //
   906  // It returns the destination Object and a possible error
   907  //
   908  // Will only be called if src.Fs().Name() == f.Name()
   909  //
   910  // If it isn't possible then return fs.ErrorCantCopy
   911  func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   912  	srcObj, ok := src.(*Object)
   913  	if !ok {
   914  		fs.Debugf(src, "Can't copy - not same remote type")
   915  		return nil, fs.ErrorCantMove
   916  	}
   917  
   918  	err := f.mkParentDir(ctx, remote)
   919  	if err != nil {
   920  		return nil, err
   921  	}
   922  	info, err := f.copyOrMove(ctx, "cp", srcObj.filePath(), remote)
   923  
   924  	if err != nil {
   925  		return nil, errors.Wrap(err, "couldn't copy file")
   926  	}
   927  
   928  	return f.newObjectWithInfo(ctx, remote, info)
   929  	//return f.newObjectWithInfo(remote, &result)
   930  }
   931  
   932  // Move src to this remote using server side move operations.
   933  //
   934  // This is stored with the remote path given
   935  //
   936  // It returns the destination Object and a possible error
   937  //
   938  // Will only be called if src.Fs().Name() == f.Name()
   939  //
   940  // If it isn't possible then return fs.ErrorCantMove
   941  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   942  	srcObj, ok := src.(*Object)
   943  	if !ok {
   944  		fs.Debugf(src, "Can't move - not same remote type")
   945  		return nil, fs.ErrorCantMove
   946  	}
   947  
   948  	err := f.mkParentDir(ctx, remote)
   949  	if err != nil {
   950  		return nil, err
   951  	}
   952  	info, err := f.copyOrMove(ctx, "mv", srcObj.filePath(), remote)
   953  
   954  	if err != nil {
   955  		return nil, errors.Wrap(err, "couldn't move file")
   956  	}
   957  
   958  	return f.newObjectWithInfo(ctx, remote, info)
   959  	//return f.newObjectWithInfo(remote, result)
   960  }
   961  
   962  // DirMove moves src, srcRemote to this remote at dstRemote
   963  // using server side move operations.
   964  //
   965  // Will only be called if src.Fs().Name() == f.Name()
   966  //
   967  // If it isn't possible then return fs.ErrorCantDirMove
   968  //
   969  // If destination exists then return fs.ErrorDirExists
   970  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
   971  	srcFs, ok := src.(*Fs)
   972  	if !ok {
   973  		fs.Debugf(srcFs, "Can't move directory - not same remote type")
   974  		return fs.ErrorCantDirMove
   975  	}
   976  	srcPath := path.Join(srcFs.root, srcRemote)
   977  	dstPath := path.Join(f.root, dstRemote)
   978  
   979  	// Refuse to move to or from the root
   980  	if srcPath == "" || dstPath == "" {
   981  		fs.Debugf(src, "DirMove error: Can't move root")
   982  		return errors.New("can't move root directory")
   983  	}
   984  	//fmt.Printf("Move src: %s (FullPath %s), dst: %s (FullPath: %s)\n", srcRemote, srcPath, dstRemote, dstPath)
   985  
   986  	var err error
   987  	_, err = f.List(ctx, dstRemote)
   988  	if err == fs.ErrorDirNotFound {
   989  		// OK
   990  	} else if err != nil {
   991  		return err
   992  	} else {
   993  		return fs.ErrorDirExists
   994  	}
   995  
   996  	_, err = f.copyOrMove(ctx, "mvDir", path.Join(f.endpointURL, f.opt.Enc.FromStandardPath(srcPath))+"/", dstRemote)
   997  
   998  	if err != nil {
   999  		return errors.Wrap(err, "couldn't move directory")
  1000  	}
  1001  	return nil
  1002  }
  1003  
  1004  // PublicLink generates a public link to the remote path (usually readable by anyone)
  1005  func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) {
  1006  	opts := rest.Opts{
  1007  		Method:     "GET",
  1008  		Path:       f.filePath(remote),
  1009  		Parameters: url.Values{},
  1010  	}
  1011  
  1012  	if f.opt.Unlink {
  1013  		opts.Parameters.Set("mode", "disableShare")
  1014  	} else {
  1015  		opts.Parameters.Set("mode", "enableShare")
  1016  	}
  1017  
  1018  	var resp *http.Response
  1019  	var result api.JottaFile
  1020  	err = f.pacer.Call(func() (bool, error) {
  1021  		resp, err = f.srv.CallXML(ctx, &opts, nil, &result)
  1022  		return shouldRetry(resp, err)
  1023  	})
  1024  
  1025  	if apiErr, ok := err.(*api.Error); ok {
  1026  		// does not exist
  1027  		if apiErr.StatusCode == http.StatusNotFound {
  1028  			return "", fs.ErrorObjectNotFound
  1029  		}
  1030  	}
  1031  	if err != nil {
  1032  		if f.opt.Unlink {
  1033  			return "", errors.Wrap(err, "couldn't remove public link")
  1034  		}
  1035  		return "", errors.Wrap(err, "couldn't create public link")
  1036  	}
  1037  	if f.opt.Unlink {
  1038  		if result.PublicSharePath != "" {
  1039  			return "", errors.Errorf("couldn't remove public link - %q", result.PublicSharePath)
  1040  		}
  1041  		return "", nil
  1042  	}
  1043  	if result.PublicSharePath == "" {
  1044  		return "", errors.New("couldn't create public link - no link path received")
  1045  	}
  1046  	link = path.Join(baseURL, result.PublicSharePath)
  1047  	return link, nil
  1048  }
  1049  
  1050  // About gets quota information
  1051  func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
  1052  	info, err := getDriveInfo(ctx, f.srv, f.user)
  1053  	if err != nil {
  1054  		return nil, err
  1055  	}
  1056  
  1057  	usage := &fs.Usage{
  1058  		Used: fs.NewUsageValue(info.Usage),
  1059  	}
  1060  	if info.Capacity > 0 {
  1061  		usage.Total = fs.NewUsageValue(info.Capacity)
  1062  		usage.Free = fs.NewUsageValue(info.Capacity - info.Usage)
  1063  	}
  1064  	return usage, nil
  1065  }
  1066  
  1067  // CleanUp empties the trash
  1068  func (f *Fs) CleanUp(ctx context.Context) error {
  1069  	opts := rest.Opts{
  1070  		Method: "POST",
  1071  		Path:   "files/v1/purge_trash",
  1072  	}
  1073  
  1074  	var info api.TrashResponse
  1075  	_, err := f.apiSrv.CallJSON(ctx, &opts, nil, &info)
  1076  	if err != nil {
  1077  		return errors.Wrap(err, "couldn't empty trash")
  1078  	}
  1079  
  1080  	return nil
  1081  }
  1082  
  1083  // Hashes returns the supported hash sets.
  1084  func (f *Fs) Hashes() hash.Set {
  1085  	return hash.Set(hash.MD5)
  1086  }
  1087  
  1088  // ---------------------------------------------
  1089  
  1090  // Fs returns the parent Fs
  1091  func (o *Object) Fs() fs.Info {
  1092  	return o.fs
  1093  }
  1094  
  1095  // Return a string version
  1096  func (o *Object) String() string {
  1097  	if o == nil {
  1098  		return "<nil>"
  1099  	}
  1100  	return o.remote
  1101  }
  1102  
  1103  // Remote returns the remote path
  1104  func (o *Object) Remote() string {
  1105  	return o.remote
  1106  }
  1107  
  1108  // filePath returns an escaped file path (f.root, remote)
  1109  func (o *Object) filePath() string {
  1110  	return o.fs.filePath(o.remote)
  1111  }
  1112  
  1113  // Hash returns the MD5 of an object returning a lowercase hex string
  1114  func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
  1115  	if t != hash.MD5 {
  1116  		return "", hash.ErrUnsupported
  1117  	}
  1118  	return o.md5, nil
  1119  }
  1120  
  1121  // Size returns the size of an object in bytes
  1122  func (o *Object) Size() int64 {
  1123  	ctx := context.TODO()
  1124  	err := o.readMetaData(ctx, false)
  1125  	if err != nil {
  1126  		fs.Logf(o, "Failed to read metadata: %v", err)
  1127  		return 0
  1128  	}
  1129  	return o.size
  1130  }
  1131  
  1132  // MimeType of an Object if known, "" otherwise
  1133  func (o *Object) MimeType(ctx context.Context) string {
  1134  	return o.mimeType
  1135  }
  1136  
  1137  // setMetaData sets the metadata from info
  1138  func (o *Object) setMetaData(info *api.JottaFile) (err error) {
  1139  	o.hasMetaData = true
  1140  	o.size = info.Size
  1141  	o.md5 = info.MD5
  1142  	o.mimeType = info.MimeType
  1143  	o.modTime = time.Time(info.ModifiedAt)
  1144  	return nil
  1145  }
  1146  
  1147  // readMetaData reads and updates the metadata for an object
  1148  func (o *Object) readMetaData(ctx context.Context, force bool) (err error) {
  1149  	if o.hasMetaData && !force {
  1150  		return nil
  1151  	}
  1152  	info, err := o.fs.readMetaDataForPath(ctx, o.remote)
  1153  	if err != nil {
  1154  		return err
  1155  	}
  1156  	if bool(info.Deleted) && !o.fs.opt.TrashedOnly {
  1157  		return fs.ErrorObjectNotFound
  1158  	}
  1159  	return o.setMetaData(info)
  1160  }
  1161  
  1162  // ModTime returns the modification time of the object
  1163  //
  1164  // It attempts to read the objects mtime and if that isn't present the
  1165  // LastModified returned in the http headers
  1166  func (o *Object) ModTime(ctx context.Context) time.Time {
  1167  	err := o.readMetaData(ctx, false)
  1168  	if err != nil {
  1169  		fs.Logf(o, "Failed to read metadata: %v", err)
  1170  		return time.Now()
  1171  	}
  1172  	return o.modTime
  1173  }
  1174  
  1175  // SetModTime sets the modification time of the local fs object
  1176  func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
  1177  	return fs.ErrorCantSetModTime
  1178  }
  1179  
  1180  // Storable returns a boolean showing whether this object storable
  1181  func (o *Object) Storable() bool {
  1182  	return true
  1183  }
  1184  
  1185  // Open an object for read
  1186  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
  1187  	fs.FixRangeOption(options, o.size)
  1188  	var resp *http.Response
  1189  	opts := rest.Opts{
  1190  		Method:     "GET",
  1191  		Path:       o.filePath(),
  1192  		Parameters: url.Values{},
  1193  		Options:    options,
  1194  	}
  1195  
  1196  	opts.Parameters.Set("mode", "bin")
  1197  
  1198  	err = o.fs.pacer.Call(func() (bool, error) {
  1199  		resp, err = o.fs.srv.Call(ctx, &opts)
  1200  		return shouldRetry(resp, err)
  1201  	})
  1202  	if err != nil {
  1203  		return nil, err
  1204  	}
  1205  	return resp.Body, err
  1206  }
  1207  
  1208  // Read the md5 of in returning a reader which will read the same contents
  1209  //
  1210  // The cleanup function should be called when out is finished with
  1211  // regardless of whether this function returned an error or not.
  1212  func readMD5(in io.Reader, size, threshold int64) (md5sum string, out io.Reader, cleanup func(), err error) {
  1213  	// we need an MD5
  1214  	md5Hasher := md5.New()
  1215  	// use the teeReader to write to the local file AND calculate the MD5 while doing so
  1216  	teeReader := io.TeeReader(in, md5Hasher)
  1217  
  1218  	// nothing to clean up by default
  1219  	cleanup = func() {}
  1220  
  1221  	// don't cache small files on disk to reduce wear of the disk
  1222  	if size > threshold {
  1223  		var tempFile *os.File
  1224  
  1225  		// create the cache file
  1226  		tempFile, err = ioutil.TempFile("", cachePrefix)
  1227  		if err != nil {
  1228  			return
  1229  		}
  1230  
  1231  		_ = os.Remove(tempFile.Name()) // Delete the file - may not work on Windows
  1232  
  1233  		// clean up the file after we are done downloading
  1234  		cleanup = func() {
  1235  			// the file should normally already be close, but just to make sure
  1236  			_ = tempFile.Close()
  1237  			_ = os.Remove(tempFile.Name()) // delete the cache file after we are done - may be deleted already
  1238  		}
  1239  
  1240  		// copy the ENTIRE file to disc and calculate the MD5 in the process
  1241  		if _, err = io.Copy(tempFile, teeReader); err != nil {
  1242  			return
  1243  		}
  1244  		// jump to the start of the local file so we can pass it along
  1245  		if _, err = tempFile.Seek(0, 0); err != nil {
  1246  			return
  1247  		}
  1248  
  1249  		// replace the already read source with a reader of our cached file
  1250  		out = tempFile
  1251  	} else {
  1252  		// that's a small file, just read it into memory
  1253  		var inData []byte
  1254  		inData, err = ioutil.ReadAll(teeReader)
  1255  		if err != nil {
  1256  			return
  1257  		}
  1258  
  1259  		// set the reader to our read memory block
  1260  		out = bytes.NewReader(inData)
  1261  	}
  1262  	return hex.EncodeToString(md5Hasher.Sum(nil)), out, cleanup, nil
  1263  }
  1264  
  1265  // Update the object with the contents of the io.Reader, modTime and size
  1266  //
  1267  // If existing is set then it updates the object rather than creating a new one
  1268  //
  1269  // The new object may have been created if an error is returned
  1270  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
  1271  	size := src.Size()
  1272  	md5String, err := src.Hash(ctx, hash.MD5)
  1273  	if err != nil || md5String == "" {
  1274  		// unwrap the accounting from the input, we use wrap to put it
  1275  		// back on after the buffering
  1276  		var wrap accounting.WrapFn
  1277  		in, wrap = accounting.UnWrap(in)
  1278  		var cleanup func()
  1279  		md5String, in, cleanup, err = readMD5(in, size, int64(o.fs.opt.MD5MemoryThreshold))
  1280  		defer cleanup()
  1281  		if err != nil {
  1282  			return errors.Wrap(err, "failed to calculate MD5")
  1283  		}
  1284  		// Wrap the accounting back onto the stream
  1285  		in = wrap(in)
  1286  	}
  1287  
  1288  	// use the api to allocate the file first and get resume / deduplication info
  1289  	var resp *http.Response
  1290  	opts := rest.Opts{
  1291  		Method:       "POST",
  1292  		Path:         "files/v1/allocate",
  1293  		Options:      options,
  1294  		ExtraHeaders: make(map[string]string),
  1295  	}
  1296  	fileDate := api.Time(src.ModTime(ctx)).APIString()
  1297  
  1298  	// the allocate request
  1299  	var request = api.AllocateFileRequest{
  1300  		Bytes:    size,
  1301  		Created:  fileDate,
  1302  		Modified: fileDate,
  1303  		Md5:      md5String,
  1304  		Path:     path.Join(o.fs.opt.Mountpoint, o.fs.opt.Enc.FromStandardPath(path.Join(o.fs.root, o.remote))),
  1305  	}
  1306  
  1307  	// send it
  1308  	var response api.AllocateFileResponse
  1309  	err = o.fs.pacer.CallNoRetry(func() (bool, error) {
  1310  		resp, err = o.fs.apiSrv.CallJSON(ctx, &opts, &request, &response)
  1311  		return shouldRetry(resp, err)
  1312  	})
  1313  	if err != nil {
  1314  		return err
  1315  	}
  1316  
  1317  	// If the file state is INCOMPLETE and CORRPUT, try to upload a then
  1318  	if response.State != "COMPLETED" {
  1319  		// how much do we still have to upload?
  1320  		remainingBytes := size - response.ResumePos
  1321  		opts = rest.Opts{
  1322  			Method:        "POST",
  1323  			RootURL:       response.UploadURL,
  1324  			ContentLength: &remainingBytes,
  1325  			ContentType:   "application/octet-stream",
  1326  			Body:          in,
  1327  			ExtraHeaders:  make(map[string]string),
  1328  		}
  1329  		if response.ResumePos != 0 {
  1330  			opts.ExtraHeaders["Range"] = "bytes=" + strconv.FormatInt(response.ResumePos, 10) + "-" + strconv.FormatInt(size-1, 10)
  1331  		}
  1332  
  1333  		// copy the already uploaded bytes into the trash :)
  1334  		var result api.UploadResponse
  1335  		_, err = io.CopyN(ioutil.Discard, in, response.ResumePos)
  1336  		if err != nil {
  1337  			return err
  1338  		}
  1339  
  1340  		// send the remaining bytes
  1341  		resp, err = o.fs.apiSrv.CallJSON(ctx, &opts, nil, &result)
  1342  		if err != nil {
  1343  			return err
  1344  		}
  1345  
  1346  		// finally update the meta data
  1347  		o.hasMetaData = true
  1348  		o.size = result.Bytes
  1349  		o.md5 = result.Md5
  1350  		o.modTime = time.Unix(result.Modified/1000, 0)
  1351  	} else {
  1352  		// If the file state is COMPLETE we don't need to upload it because the file was already found but we still ned to update our metadata
  1353  		return o.readMetaData(ctx, true)
  1354  	}
  1355  
  1356  	return nil
  1357  }
  1358  
  1359  // Remove an object
  1360  func (o *Object) Remove(ctx context.Context) error {
  1361  	opts := rest.Opts{
  1362  		Method:     "POST",
  1363  		Path:       o.filePath(),
  1364  		Parameters: url.Values{},
  1365  		NoResponse: true,
  1366  	}
  1367  
  1368  	if o.fs.opt.HardDelete {
  1369  		opts.Parameters.Set("rm", "true")
  1370  	} else {
  1371  		opts.Parameters.Set("dl", "true")
  1372  	}
  1373  
  1374  	return o.fs.pacer.Call(func() (bool, error) {
  1375  		resp, err := o.fs.srv.CallXML(ctx, &opts, nil, nil)
  1376  		return shouldRetry(resp, err)
  1377  	})
  1378  }
  1379  
  1380  // Check the interfaces are satisfied
  1381  var (
  1382  	_ fs.Fs           = (*Fs)(nil)
  1383  	_ fs.Purger       = (*Fs)(nil)
  1384  	_ fs.Copier       = (*Fs)(nil)
  1385  	_ fs.Mover        = (*Fs)(nil)
  1386  	_ fs.DirMover     = (*Fs)(nil)
  1387  	_ fs.ListRer      = (*Fs)(nil)
  1388  	_ fs.PublicLinker = (*Fs)(nil)
  1389  	_ fs.Abouter      = (*Fs)(nil)
  1390  	_ fs.CleanUpper   = (*Fs)(nil)
  1391  	_ fs.Object       = (*Object)(nil)
  1392  	_ fs.MimeTyper    = (*Object)(nil)
  1393  )