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

     1  package seafile
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/url"
    11  	"path"
    12  	"strings"
    13  
    14  	"github.com/pkg/errors"
    15  	"github.com/rclone/rclone/backend/seafile/api"
    16  	"github.com/rclone/rclone/fs"
    17  	"github.com/rclone/rclone/lib/readers"
    18  	"github.com/rclone/rclone/lib/rest"
    19  )
    20  
    21  // Start of the API URLs
    22  const (
    23  	APIv20 = "api2/repos/"
    24  	APIv21 = "api/v2.1/repos/"
    25  )
    26  
    27  // Errors specific to seafile fs
    28  var (
    29  	ErrorInternalDuringUpload = errors.New("Internal server error during file upload")
    30  )
    31  
    32  // ==================== Seafile API ====================
    33  
    34  func (f *Fs) getAuthorizationToken(ctx context.Context) (string, error) {
    35  	return getAuthorizationToken(ctx, f.srv, f.opt.User, f.opt.Password, "")
    36  }
    37  
    38  // getAuthorizationToken can be called outside of an fs (during configuration of the remote to get the authentication token)
    39  // it's doing a single call (no pacer involved)
    40  func getAuthorizationToken(ctx context.Context, srv *rest.Client, user, password, oneTimeCode string) (string, error) {
    41  	// API Documentation
    42  	// https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start
    43  	opts := rest.Opts{
    44  		Method:       "POST",
    45  		Path:         "api2/auth-token/",
    46  		ExtraHeaders: map[string]string{"Authorization": ""}, // unset the Authorization for this request
    47  		IgnoreStatus: true,                                   // so we can load the error messages back into result
    48  	}
    49  
    50  	// 2FA
    51  	if oneTimeCode != "" {
    52  		opts.ExtraHeaders["X-SEAFILE-OTP"] = oneTimeCode
    53  	}
    54  
    55  	request := api.AuthenticationRequest{
    56  		Username: user,
    57  		Password: password,
    58  	}
    59  	result := api.AuthenticationResult{}
    60  
    61  	_, err := srv.CallJSON(ctx, &opts, &request, &result)
    62  	if err != nil {
    63  		// This is only going to be http errors here
    64  		return "", errors.Wrap(err, "failed to authenticate")
    65  	}
    66  	if result.Errors != nil && len(result.Errors) > 0 {
    67  		return "", errors.New(strings.Join(result.Errors, ", "))
    68  	}
    69  	if result.Token == "" {
    70  		// No error in "non_field_errors" field but still empty token
    71  		return "", errors.New("failed to authenticate")
    72  	}
    73  	return result.Token, nil
    74  }
    75  
    76  func (f *Fs) getServerInfo(ctx context.Context) (account *api.ServerInfo, err error) {
    77  	// API Documentation
    78  	// https://download.seafile.com/published/web-api/v2.1/server-info.md#user-content-Get%20Server%20Information
    79  	opts := rest.Opts{
    80  		Method: "GET",
    81  		Path:   "api2/server-info/",
    82  	}
    83  
    84  	result := api.ServerInfo{}
    85  
    86  	var resp *http.Response
    87  	err = f.pacer.Call(func() (bool, error) {
    88  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
    89  		return f.shouldRetry(resp, err)
    90  	})
    91  	if err != nil {
    92  		if resp != nil {
    93  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
    94  				return nil, fs.ErrorPermissionDenied
    95  			}
    96  		}
    97  		return nil, errors.Wrap(err, "failed to get server info")
    98  	}
    99  	return &result, nil
   100  }
   101  
   102  func (f *Fs) getUserAccountInfo(ctx context.Context) (account *api.AccountInfo, err error) {
   103  	// API Documentation
   104  	// https://download.seafile.com/published/web-api/v2.1/account.md#user-content-Check%20Account%20Info
   105  	opts := rest.Opts{
   106  		Method: "GET",
   107  		Path:   "api2/account/info/",
   108  	}
   109  
   110  	result := api.AccountInfo{}
   111  
   112  	var resp *http.Response
   113  	err = f.pacer.Call(func() (bool, error) {
   114  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   115  		return f.shouldRetry(resp, err)
   116  	})
   117  	if err != nil {
   118  		if resp != nil {
   119  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   120  				return nil, fs.ErrorPermissionDenied
   121  			}
   122  		}
   123  		return nil, errors.Wrap(err, "failed to get account info")
   124  	}
   125  	return &result, nil
   126  }
   127  
   128  func (f *Fs) getLibraries(ctx context.Context) ([]api.Library, error) {
   129  	// API Documentation
   130  	// https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-List%20Libraries
   131  	opts := rest.Opts{
   132  		Method: "GET",
   133  		Path:   APIv20,
   134  	}
   135  
   136  	result := make([]api.Library, 1)
   137  
   138  	var resp *http.Response
   139  	var err error
   140  	err = f.pacer.Call(func() (bool, error) {
   141  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   142  		return f.shouldRetry(resp, err)
   143  	})
   144  	if err != nil {
   145  		if resp != nil {
   146  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   147  				return nil, fs.ErrorPermissionDenied
   148  			}
   149  		}
   150  		return nil, errors.Wrap(err, "failed to get libraries")
   151  	}
   152  	return result, nil
   153  }
   154  
   155  func (f *Fs) createLibrary(ctx context.Context, libraryName, password string) (library *api.CreateLibrary, err error) {
   156  	// API Documentation
   157  	// https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Create%20Library
   158  	opts := rest.Opts{
   159  		Method: "POST",
   160  		Path:   APIv20,
   161  	}
   162  
   163  	request := api.CreateLibraryRequest{
   164  		Name:        f.opt.Enc.FromStandardName(libraryName),
   165  		Description: "Created by rclone",
   166  		Password:    password,
   167  	}
   168  	result := &api.CreateLibrary{}
   169  
   170  	var resp *http.Response
   171  	err = f.pacer.Call(func() (bool, error) {
   172  		resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
   173  		return f.shouldRetry(resp, err)
   174  	})
   175  	if err != nil {
   176  		if resp != nil {
   177  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   178  				return nil, fs.ErrorPermissionDenied
   179  			}
   180  		}
   181  		return nil, errors.Wrap(err, "failed to create library")
   182  	}
   183  	return result, nil
   184  }
   185  
   186  func (f *Fs) deleteLibrary(ctx context.Context, libraryID string) error {
   187  	// API Documentation
   188  	// https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Create%20Library
   189  	opts := rest.Opts{
   190  		Method: "DELETE",
   191  		Path:   APIv20 + libraryID + "/",
   192  	}
   193  
   194  	result := ""
   195  
   196  	var resp *http.Response
   197  	var err error
   198  	err = f.pacer.Call(func() (bool, error) {
   199  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   200  		return f.shouldRetry(resp, err)
   201  	})
   202  	if err != nil {
   203  		if resp != nil {
   204  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   205  				return fs.ErrorPermissionDenied
   206  			}
   207  		}
   208  		return errors.Wrap(err, "failed to delete library")
   209  	}
   210  	return nil
   211  }
   212  
   213  func (f *Fs) decryptLibrary(ctx context.Context, libraryID, password string) error {
   214  	// API Documentation
   215  	// https://download.seafile.com/published/web-api/v2.1/library-encryption.md#user-content-Decrypt%20Library
   216  	if libraryID == "" {
   217  		return errors.New("cannot list files without a library")
   218  	}
   219  	// This is another call that cannot accept a JSON input so we have to build it manually
   220  	opts := rest.Opts{
   221  		Method:      "POST",
   222  		Path:        APIv20 + libraryID + "/",
   223  		ContentType: "application/x-www-form-urlencoded",
   224  		Body:        bytes.NewBuffer([]byte("password=" + f.opt.Enc.FromStandardName(password))),
   225  		NoResponse:  true,
   226  	}
   227  	var resp *http.Response
   228  	var err error
   229  	err = f.pacer.Call(func() (bool, error) {
   230  		resp, err = f.srv.Call(ctx, &opts)
   231  		return f.shouldRetry(resp, err)
   232  	})
   233  	if err != nil {
   234  		if resp != nil {
   235  			if resp.StatusCode == 400 {
   236  				return errors.New("incorrect password")
   237  			}
   238  			if resp.StatusCode == 409 {
   239  				fs.Debugf(nil, "library is not encrypted")
   240  				return nil
   241  			}
   242  		}
   243  		return errors.Wrap(err, "failed to decrypt library")
   244  	}
   245  	return nil
   246  }
   247  
   248  func (f *Fs) getDirectoryEntriesAPIv21(ctx context.Context, libraryID, dirPath string, recursive bool) ([]api.DirEntry, error) {
   249  	// API Documentation
   250  	// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-List%20Items%20in%20Directory
   251  	// This is using the undocumented version 2.1 of the API (so we can use the recursive option which is not available in the version 2)
   252  	if libraryID == "" {
   253  		return nil, errors.New("cannot list files without a library")
   254  	}
   255  	dirPath = path.Join("/", dirPath)
   256  
   257  	recursiveFlag := "0"
   258  	if recursive {
   259  		recursiveFlag = "1"
   260  	}
   261  	opts := rest.Opts{
   262  		Method: "GET",
   263  		Path:   APIv21 + libraryID + "/dir/",
   264  		Parameters: url.Values{
   265  			"recursive": {recursiveFlag},
   266  			"p":         {f.opt.Enc.FromStandardPath(dirPath)},
   267  		},
   268  	}
   269  	result := &api.DirEntries{}
   270  	var resp *http.Response
   271  	var err error
   272  	err = f.pacer.Call(func() (bool, error) {
   273  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   274  		return f.shouldRetry(resp, err)
   275  	})
   276  	if err != nil {
   277  		if resp != nil {
   278  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   279  				return nil, fs.ErrorPermissionDenied
   280  			}
   281  			if resp.StatusCode == 404 {
   282  				return nil, fs.ErrorDirNotFound
   283  			}
   284  			if resp.StatusCode == 440 {
   285  				// Encrypted library and password not provided
   286  				return nil, fs.ErrorPermissionDenied
   287  			}
   288  		}
   289  		return nil, errors.Wrap(err, "failed to get directory contents")
   290  	}
   291  
   292  	// Clean up encoded names
   293  	for index, fileInfo := range result.Entries {
   294  		fileInfo.Name = f.opt.Enc.ToStandardName(fileInfo.Name)
   295  		fileInfo.Path = f.opt.Enc.ToStandardPath(fileInfo.Path)
   296  		result.Entries[index] = fileInfo
   297  	}
   298  	return result.Entries, nil
   299  }
   300  
   301  func (f *Fs) getDirectoryDetails(ctx context.Context, libraryID, dirPath string) (*api.DirectoryDetail, error) {
   302  	// API Documentation
   303  	// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Get%20Directory%20Detail
   304  	if libraryID == "" {
   305  		return nil, errors.New("cannot read directory without a library")
   306  	}
   307  	dirPath = path.Join("/", dirPath)
   308  
   309  	opts := rest.Opts{
   310  		Method:     "GET",
   311  		Path:       APIv21 + libraryID + "/dir/detail/",
   312  		Parameters: url.Values{"path": {f.opt.Enc.FromStandardPath(dirPath)}},
   313  	}
   314  	result := &api.DirectoryDetail{}
   315  	var resp *http.Response
   316  	var err error
   317  	err = f.pacer.Call(func() (bool, error) {
   318  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   319  		return f.shouldRetry(resp, err)
   320  	})
   321  	if err != nil {
   322  		if resp != nil {
   323  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   324  				return nil, fs.ErrorPermissionDenied
   325  			}
   326  			if resp.StatusCode == 404 {
   327  				return nil, fs.ErrorDirNotFound
   328  			}
   329  		}
   330  		return nil, errors.Wrap(err, "failed to get directory details")
   331  	}
   332  	result.Name = f.opt.Enc.ToStandardName(result.Name)
   333  	result.Path = f.opt.Enc.ToStandardPath(result.Path)
   334  	return result, nil
   335  }
   336  
   337  // createDir creates a new directory. The API will add a number to the directory name if it already exist
   338  func (f *Fs) createDir(ctx context.Context, libraryID, dirPath string) error {
   339  	// API Documentation
   340  	// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Create%20New%20Directory
   341  	if libraryID == "" {
   342  		return errors.New("cannot create directory without a library")
   343  	}
   344  	dirPath = path.Join("/", dirPath)
   345  
   346  	// This call *cannot* handle json parameters in the body, so we have to build the request body manually
   347  	opts := rest.Opts{
   348  		Method:      "POST",
   349  		Path:        APIv20 + libraryID + "/dir/",
   350  		Parameters:  url.Values{"p": {f.opt.Enc.FromStandardPath(dirPath)}},
   351  		NoRedirect:  true,
   352  		ContentType: "application/x-www-form-urlencoded",
   353  		Body:        bytes.NewBuffer([]byte("operation=mkdir")),
   354  		NoResponse:  true,
   355  	}
   356  
   357  	var resp *http.Response
   358  	var err error
   359  	err = f.pacer.Call(func() (bool, error) {
   360  		resp, err = f.srv.Call(ctx, &opts)
   361  		return f.shouldRetry(resp, err)
   362  	})
   363  	if err != nil {
   364  		if resp != nil {
   365  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   366  				return fs.ErrorPermissionDenied
   367  			}
   368  		}
   369  		return errors.Wrap(err, "failed to create directory")
   370  	}
   371  	return nil
   372  }
   373  
   374  func (f *Fs) renameDir(ctx context.Context, libraryID, dirPath, newName string) error {
   375  	// API Documentation
   376  	// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Rename%20Directory
   377  	if libraryID == "" {
   378  		return errors.New("cannot rename directory without a library")
   379  	}
   380  	dirPath = path.Join("/", dirPath)
   381  
   382  	// This call *cannot* handle json parameters in the body, so we have to build the request body manually
   383  	postParameters := url.Values{
   384  		"operation": {"rename"},
   385  		"newname":   {f.opt.Enc.FromStandardPath(newName)},
   386  	}
   387  
   388  	opts := rest.Opts{
   389  		Method:      "POST",
   390  		Path:        APIv20 + libraryID + "/dir/",
   391  		Parameters:  url.Values{"p": {f.opt.Enc.FromStandardPath(dirPath)}},
   392  		ContentType: "application/x-www-form-urlencoded",
   393  		Body:        bytes.NewBuffer([]byte(postParameters.Encode())),
   394  		NoResponse:  true,
   395  	}
   396  
   397  	var resp *http.Response
   398  	var err error
   399  	err = f.pacer.Call(func() (bool, error) {
   400  		resp, err = f.srv.Call(ctx, &opts)
   401  		return f.shouldRetry(resp, err)
   402  	})
   403  	if err != nil {
   404  		if resp != nil {
   405  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   406  				return fs.ErrorPermissionDenied
   407  			}
   408  		}
   409  		return errors.Wrap(err, "failed to rename directory")
   410  	}
   411  	return nil
   412  }
   413  
   414  func (f *Fs) moveDir(ctx context.Context, srcLibraryID, srcDir, srcName, dstLibraryID, dstPath string) error {
   415  	// API Documentation
   416  	// https://download.seafile.com/published/web-api/v2.1/files-directories-batch-op.md#user-content-Batch%20Move%20Items%20Synchronously
   417  	if srcLibraryID == "" || dstLibraryID == "" || srcName == "" {
   418  		return errors.New("libraryID and/or file path argument(s) missing")
   419  	}
   420  	srcDir = path.Join("/", srcDir)
   421  	dstPath = path.Join("/", dstPath)
   422  
   423  	opts := rest.Opts{
   424  		Method:     "POST",
   425  		Path:       APIv21 + "sync-batch-move-item/",
   426  		NoResponse: true,
   427  	}
   428  
   429  	request := &api.BatchSourceDestRequest{
   430  		SrcLibraryID: srcLibraryID,
   431  		SrcParentDir: f.opt.Enc.FromStandardPath(srcDir),
   432  		SrcItems:     []string{f.opt.Enc.FromStandardPath(srcName)},
   433  		DstLibraryID: dstLibraryID,
   434  		DstParentDir: f.opt.Enc.FromStandardPath(dstPath),
   435  	}
   436  
   437  	var resp *http.Response
   438  	var err error
   439  	err = f.pacer.Call(func() (bool, error) {
   440  		resp, err = f.srv.CallJSON(ctx, &opts, &request, nil)
   441  		return f.shouldRetry(resp, err)
   442  	})
   443  	if err != nil {
   444  		if resp != nil {
   445  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   446  				return fs.ErrorPermissionDenied
   447  			}
   448  			if resp.StatusCode == 404 {
   449  				return fs.ErrorObjectNotFound
   450  			}
   451  		}
   452  		return errors.Wrap(err, fmt.Sprintf("failed to move directory '%s' from '%s' to '%s'", srcName, srcDir, dstPath))
   453  	}
   454  
   455  	return nil
   456  }
   457  
   458  func (f *Fs) deleteDir(ctx context.Context, libraryID, filePath string) error {
   459  	// API Documentation
   460  	// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Delete%20Directory
   461  	if libraryID == "" {
   462  		return errors.New("cannot delete directory without a library")
   463  	}
   464  	filePath = path.Join("/", filePath)
   465  
   466  	opts := rest.Opts{
   467  		Method:     "DELETE",
   468  		Path:       APIv20 + libraryID + "/dir/",
   469  		Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
   470  		NoResponse: true,
   471  	}
   472  
   473  	var resp *http.Response
   474  	var err error
   475  	err = f.pacer.Call(func() (bool, error) {
   476  		resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
   477  		return f.shouldRetry(resp, err)
   478  	})
   479  	if err != nil {
   480  		if resp != nil {
   481  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   482  				return fs.ErrorPermissionDenied
   483  			}
   484  		}
   485  		return errors.Wrap(err, "failed to delete directory")
   486  	}
   487  	return nil
   488  }
   489  
   490  func (f *Fs) getFileDetails(ctx context.Context, libraryID, filePath string) (*api.FileDetail, error) {
   491  	// API Documentation
   492  	// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Get%20File%20Detail
   493  	if libraryID == "" {
   494  		return nil, errors.New("cannot open file without a library")
   495  	}
   496  	filePath = path.Join("/", filePath)
   497  
   498  	opts := rest.Opts{
   499  		Method:     "GET",
   500  		Path:       APIv20 + libraryID + "/file/detail/",
   501  		Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
   502  	}
   503  	result := &api.FileDetail{}
   504  	var resp *http.Response
   505  	var err error
   506  	err = f.pacer.Call(func() (bool, error) {
   507  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   508  		return f.shouldRetry(resp, err)
   509  	})
   510  	if err != nil {
   511  		if resp != nil {
   512  			if resp.StatusCode == 404 {
   513  				return nil, fs.ErrorObjectNotFound
   514  			}
   515  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   516  				return nil, fs.ErrorPermissionDenied
   517  			}
   518  		}
   519  		return nil, errors.Wrap(err, "failed to get file details")
   520  	}
   521  	result.Name = f.opt.Enc.ToStandardName(result.Name)
   522  	result.Parent = f.opt.Enc.ToStandardPath(result.Parent)
   523  	return result, nil
   524  }
   525  
   526  func (f *Fs) deleteFile(ctx context.Context, libraryID, filePath string) error {
   527  	// API Documentation
   528  	// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Delete%20File
   529  	if libraryID == "" {
   530  		return errors.New("cannot delete file without a library")
   531  	}
   532  	filePath = path.Join("/", filePath)
   533  
   534  	opts := rest.Opts{
   535  		Method:     "DELETE",
   536  		Path:       APIv20 + libraryID + "/file/",
   537  		Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
   538  		NoResponse: true,
   539  	}
   540  	err := f.pacer.Call(func() (bool, error) {
   541  		resp, err := f.srv.CallJSON(ctx, &opts, nil, nil)
   542  		return f.shouldRetry(resp, err)
   543  	})
   544  	if err != nil {
   545  		return errors.Wrap(err, "failed to delete file")
   546  	}
   547  	return nil
   548  }
   549  
   550  func (f *Fs) getDownloadLink(ctx context.Context, libraryID, filePath string) (string, error) {
   551  	// API Documentation
   552  	// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Download%20File
   553  	if libraryID == "" {
   554  		return "", errors.New("cannot download file without a library")
   555  	}
   556  	filePath = path.Join("/", filePath)
   557  
   558  	opts := rest.Opts{
   559  		Method:     "GET",
   560  		Path:       APIv20 + libraryID + "/file/",
   561  		Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
   562  	}
   563  	result := ""
   564  	var resp *http.Response
   565  	var err error
   566  	err = f.pacer.Call(func() (bool, error) {
   567  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   568  		return f.shouldRetry(resp, err)
   569  	})
   570  	if err != nil {
   571  		if resp != nil {
   572  			if resp.StatusCode == 404 {
   573  				return "", fs.ErrorObjectNotFound
   574  			}
   575  		}
   576  		return "", errors.Wrap(err, "failed to get download link")
   577  	}
   578  	return result, nil
   579  }
   580  
   581  func (f *Fs) download(ctx context.Context, url string, size int64, options ...fs.OpenOption) (io.ReadCloser, error) {
   582  	// Check if we need to download partial content
   583  	var start, end int64 = 0, size
   584  	partialContent := false
   585  	for _, option := range options {
   586  		switch x := option.(type) {
   587  		case *fs.SeekOption:
   588  			start = x.Offset
   589  			partialContent = true
   590  		case *fs.RangeOption:
   591  			if x.Start >= 0 {
   592  				start = x.Start
   593  				if x.End > 0 && x.End < size {
   594  					end = x.End + 1
   595  				}
   596  			} else {
   597  				// {-1, 20} should load the last 20 characters [len-20:len]
   598  				start = size - x.End
   599  			}
   600  			partialContent = true
   601  		default:
   602  			if option.Mandatory() {
   603  				fs.Logf(nil, "Unsupported mandatory option: %v", option)
   604  			}
   605  		}
   606  	}
   607  	// Build the http request
   608  	opts := rest.Opts{
   609  		Method:  "GET",
   610  		RootURL: url,
   611  		Options: options,
   612  	}
   613  	var resp *http.Response
   614  	var err error
   615  	err = f.pacer.Call(func() (bool, error) {
   616  		resp, err = f.srv.Call(ctx, &opts)
   617  		return f.shouldRetry(resp, err)
   618  	})
   619  	if err != nil {
   620  		if resp != nil {
   621  			if resp.StatusCode == 404 {
   622  				return nil, fmt.Errorf("file not found '%s'", url)
   623  			}
   624  		}
   625  		return nil, err
   626  	}
   627  	// Non-encrypted libraries are accepting the HTTP Range header,
   628  	// BUT encrypted libraries are simply ignoring it
   629  	if partialContent && resp.StatusCode == 200 {
   630  		// Partial content was requested through a Range header, but a full content was sent instead
   631  		rangeDownloadNotice.Do(func() {
   632  			fs.Logf(nil, "%s ignored our request of partial content. This is probably because encrypted libraries are not accepting range requests. Loading this file might be slow!", f.String())
   633  		})
   634  		if start > 0 {
   635  			// We need to read and discard the beginning of the data...
   636  			_, err = io.CopyN(ioutil.Discard, resp.Body, start)
   637  			if err != nil {
   638  				return nil, err
   639  			}
   640  		}
   641  		// ... and return a limited reader for the remaining of the data
   642  		return readers.NewLimitedReadCloser(resp.Body, end-start), nil
   643  	}
   644  	return resp.Body, nil
   645  }
   646  
   647  func (f *Fs) getUploadLink(ctx context.Context, libraryID string) (string, error) {
   648  	// API Documentation
   649  	// https://download.seafile.com/published/web-api/v2.1/file-upload.md
   650  	if libraryID == "" {
   651  		return "", errors.New("cannot upload file without a library")
   652  	}
   653  	opts := rest.Opts{
   654  		Method: "GET",
   655  		Path:   APIv20 + libraryID + "/upload-link/",
   656  	}
   657  	result := ""
   658  	var resp *http.Response
   659  	var err error
   660  	err = f.pacer.Call(func() (bool, error) {
   661  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   662  		return f.shouldRetry(resp, err)
   663  	})
   664  	if err != nil {
   665  		if resp != nil {
   666  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   667  				return "", fs.ErrorPermissionDenied
   668  			}
   669  		}
   670  		return "", errors.Wrap(err, "failed to get upload link")
   671  	}
   672  	return result, nil
   673  }
   674  
   675  func (f *Fs) upload(ctx context.Context, in io.Reader, uploadLink, filePath string) (*api.FileDetail, error) {
   676  	// API Documentation
   677  	// https://download.seafile.com/published/web-api/v2.1/file-upload.md
   678  	fileDir, filename := path.Split(filePath)
   679  	parameters := url.Values{
   680  		"parent_dir":        {"/"},
   681  		"relative_path":     {f.opt.Enc.FromStandardPath(fileDir)},
   682  		"need_idx_progress": {"true"},
   683  		"replace":           {"1"},
   684  	}
   685  	formReader, contentType, _, err := rest.MultipartUpload(in, parameters, "file", f.opt.Enc.FromStandardName(filename))
   686  	if err != nil {
   687  		return nil, errors.Wrap(err, "failed to make multipart upload")
   688  	}
   689  
   690  	opts := rest.Opts{
   691  		Method:      "POST",
   692  		RootURL:     uploadLink,
   693  		Body:        formReader,
   694  		ContentType: contentType,
   695  		Parameters:  url.Values{"ret-json": {"1"}}, // It needs to be on the url, not in the body parameters
   696  	}
   697  	result := make([]api.FileDetail, 1)
   698  	var resp *http.Response
   699  	// If an error occurs during the call, do not attempt to retry: The upload link is single use only
   700  	err = f.pacer.CallNoRetry(func() (bool, error) {
   701  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   702  		return f.shouldRetryUpload(ctx, resp, err)
   703  	})
   704  	if err != nil {
   705  		if resp != nil {
   706  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   707  				return nil, fs.ErrorPermissionDenied
   708  			}
   709  			if resp.StatusCode == 500 {
   710  				// This is a temporary error - we will get a new upload link before retrying
   711  				return nil, ErrorInternalDuringUpload
   712  			}
   713  		}
   714  		return nil, errors.Wrap(err, "failed to upload file")
   715  	}
   716  	if len(result) > 0 {
   717  		result[0].Parent = f.opt.Enc.ToStandardPath(result[0].Parent)
   718  		result[0].Name = f.opt.Enc.ToStandardName(result[0].Name)
   719  		return &result[0], nil
   720  	}
   721  	return nil, nil
   722  }
   723  
   724  func (f *Fs) listShareLinks(ctx context.Context, libraryID, remote string) ([]api.SharedLink, error) {
   725  	// API Documentation
   726  	// https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20Share%20Link%20of%20a%20Folder%20(File)
   727  	if libraryID == "" {
   728  		return nil, errors.New("cannot get share links without a library")
   729  	}
   730  	remote = path.Join("/", remote)
   731  
   732  	opts := rest.Opts{
   733  		Method:     "GET",
   734  		Path:       "api/v2.1/share-links/",
   735  		Parameters: url.Values{"repo_id": {libraryID}, "path": {f.opt.Enc.FromStandardPath(remote)}},
   736  	}
   737  	result := make([]api.SharedLink, 1)
   738  	var resp *http.Response
   739  	var err error
   740  	err = f.pacer.Call(func() (bool, error) {
   741  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   742  		return f.shouldRetry(resp, err)
   743  	})
   744  	if err != nil {
   745  		if resp != nil {
   746  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   747  				return nil, fs.ErrorPermissionDenied
   748  			}
   749  			if resp.StatusCode == 404 {
   750  				return nil, fs.ErrorObjectNotFound
   751  			}
   752  		}
   753  		return nil, errors.Wrap(err, "failed to list shared links")
   754  	}
   755  	return result, nil
   756  }
   757  
   758  // createShareLink will only work with non-encrypted libraries
   759  func (f *Fs) createShareLink(ctx context.Context, libraryID, remote string) (*api.SharedLink, error) {
   760  	// API Documentation
   761  	// https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-Create%20Share%20Link
   762  	if libraryID == "" {
   763  		return nil, errors.New("cannot create a shared link without a library")
   764  	}
   765  	remote = path.Join("/", remote)
   766  
   767  	opts := rest.Opts{
   768  		Method: "POST",
   769  		Path:   "api/v2.1/share-links/",
   770  	}
   771  	request := &api.ShareLinkRequest{
   772  		LibraryID: libraryID,
   773  		Path:      f.opt.Enc.FromStandardPath(remote),
   774  	}
   775  	result := &api.SharedLink{}
   776  	var resp *http.Response
   777  	var err error
   778  	err = f.pacer.Call(func() (bool, error) {
   779  		resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
   780  		return f.shouldRetry(resp, err)
   781  	})
   782  	if err != nil {
   783  		if resp != nil {
   784  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   785  				return nil, fs.ErrorPermissionDenied
   786  			}
   787  			if resp.StatusCode == 404 {
   788  				return nil, fs.ErrorObjectNotFound
   789  			}
   790  		}
   791  		return nil, errors.Wrap(err, "failed to create a shared link")
   792  	}
   793  	return result, nil
   794  }
   795  
   796  func (f *Fs) copyFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID, dstPath string) (*api.FileInfo, error) {
   797  	// API Documentation
   798  	// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Copy%20File
   799  	// It's using the api/v2.1 which is not in the documentation (as of Apr 2020) but works better than api2
   800  	if srcLibraryID == "" || dstLibraryID == "" {
   801  		return nil, errors.New("libraryID and/or file path argument(s) missing")
   802  	}
   803  	srcPath = path.Join("/", srcPath)
   804  	dstPath = path.Join("/", dstPath)
   805  
   806  	opts := rest.Opts{
   807  		Method:     "POST",
   808  		Path:       APIv21 + srcLibraryID + "/file/",
   809  		Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(srcPath)}},
   810  	}
   811  	request := &api.FileOperationRequest{
   812  		Operation:            api.CopyFileOperation,
   813  		DestinationLibraryID: dstLibraryID,
   814  		DestinationPath:      f.opt.Enc.FromStandardPath(dstPath),
   815  	}
   816  	result := &api.FileInfo{}
   817  	var resp *http.Response
   818  	var err error
   819  	err = f.pacer.Call(func() (bool, error) {
   820  		resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
   821  		return f.shouldRetry(resp, err)
   822  	})
   823  	if err != nil {
   824  		if resp != nil {
   825  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   826  				return nil, fs.ErrorPermissionDenied
   827  			}
   828  			if resp.StatusCode == 404 {
   829  				fs.Debugf(nil, "Copy: %s", err)
   830  				return nil, fs.ErrorObjectNotFound
   831  			}
   832  		}
   833  		return nil, errors.Wrap(err, fmt.Sprintf("failed to copy file %s:'%s' to %s:'%s'", srcLibraryID, srcPath, dstLibraryID, dstPath))
   834  	}
   835  	return f.decodeFileInfo(result), nil
   836  }
   837  
   838  func (f *Fs) moveFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID, dstPath string) (*api.FileInfo, error) {
   839  	// API Documentation
   840  	// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Move%20File
   841  	// It's using the api/v2.1 which is not in the documentation (as of Apr 2020) but works better than api2
   842  	if srcLibraryID == "" || dstLibraryID == "" {
   843  		return nil, errors.New("libraryID and/or file path argument(s) missing")
   844  	}
   845  	srcPath = path.Join("/", srcPath)
   846  	dstPath = path.Join("/", dstPath)
   847  
   848  	opts := rest.Opts{
   849  		Method:     "POST",
   850  		Path:       APIv21 + srcLibraryID + "/file/",
   851  		Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(srcPath)}},
   852  	}
   853  	request := &api.FileOperationRequest{
   854  		Operation:            api.MoveFileOperation,
   855  		DestinationLibraryID: dstLibraryID,
   856  		DestinationPath:      f.opt.Enc.FromStandardPath(dstPath),
   857  	}
   858  	result := &api.FileInfo{}
   859  	var resp *http.Response
   860  	var err error
   861  	err = f.pacer.Call(func() (bool, error) {
   862  		resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
   863  		return f.shouldRetry(resp, err)
   864  	})
   865  	if err != nil {
   866  		if resp != nil {
   867  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   868  				return nil, fs.ErrorPermissionDenied
   869  			}
   870  			if resp.StatusCode == 404 {
   871  				fs.Debugf(nil, "Move: %s", err)
   872  				return nil, fs.ErrorObjectNotFound
   873  			}
   874  		}
   875  		return nil, errors.Wrap(err, fmt.Sprintf("failed to move file %s:'%s' to %s:'%s'", srcLibraryID, srcPath, dstLibraryID, dstPath))
   876  	}
   877  	return f.decodeFileInfo(result), nil
   878  }
   879  
   880  func (f *Fs) renameFile(ctx context.Context, libraryID, filePath, newname string) (*api.FileInfo, error) {
   881  	// API Documentation
   882  	// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Rename%20File
   883  	// It's using the api/v2.1 which is not in the documentation (as of Apr 2020) but works better than api2
   884  	if libraryID == "" || newname == "" {
   885  		return nil, errors.New("libraryID and/or file path argument(s) missing")
   886  	}
   887  	filePath = path.Join("/", filePath)
   888  
   889  	opts := rest.Opts{
   890  		Method:     "POST",
   891  		Path:       APIv21 + libraryID + "/file/",
   892  		Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
   893  	}
   894  	request := &api.FileOperationRequest{
   895  		Operation: api.RenameFileOperation,
   896  		NewName:   f.opt.Enc.FromStandardName(newname),
   897  	}
   898  	result := &api.FileInfo{}
   899  	var resp *http.Response
   900  	var err error
   901  	err = f.pacer.Call(func() (bool, error) {
   902  		resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
   903  		return f.shouldRetry(resp, err)
   904  	})
   905  	if err != nil {
   906  		if resp != nil {
   907  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   908  				return nil, fs.ErrorPermissionDenied
   909  			}
   910  			if resp.StatusCode == 404 {
   911  				fs.Debugf(nil, "Rename: %s", err)
   912  				return nil, fs.ErrorObjectNotFound
   913  			}
   914  		}
   915  		return nil, errors.Wrap(err, fmt.Sprintf("failed to rename file '%s' to '%s'", filePath, newname))
   916  	}
   917  	return f.decodeFileInfo(result), nil
   918  }
   919  
   920  func (f *Fs) decodeFileInfo(input *api.FileInfo) *api.FileInfo {
   921  	input.Name = f.opt.Enc.ToStandardName(input.Name)
   922  	input.Path = f.opt.Enc.ToStandardPath(input.Path)
   923  	return input
   924  }
   925  
   926  func (f *Fs) emptyLibraryTrash(ctx context.Context, libraryID string) error {
   927  	// API Documentation
   928  	// https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Clean%20Library%20Trash
   929  	if libraryID == "" {
   930  		return errors.New("cannot clean up trash without a library")
   931  	}
   932  	opts := rest.Opts{
   933  		Method:     "DELETE",
   934  		Path:       APIv21 + libraryID + "/trash/",
   935  		NoResponse: true,
   936  	}
   937  	var resp *http.Response
   938  	var err error
   939  	err = f.pacer.Call(func() (bool, error) {
   940  		resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
   941  		return f.shouldRetry(resp, err)
   942  	})
   943  	if err != nil {
   944  		if resp != nil {
   945  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   946  				return fs.ErrorPermissionDenied
   947  			}
   948  			if resp.StatusCode == 404 {
   949  				return fs.ErrorObjectNotFound
   950  			}
   951  		}
   952  		return errors.Wrap(err, "failed empty the library trash")
   953  	}
   954  	return nil
   955  }
   956  
   957  // === API v2 from the official documentation, but that have been replaced by the much better v2.1 (undocumented as of Apr 2020)
   958  // === getDirectoryEntriesAPIv2 is needed to keep compatibility with seafile v6,
   959  // === the others can probably be removed after the API v2.1 is documented
   960  
   961  func (f *Fs) getDirectoryEntriesAPIv2(ctx context.Context, libraryID, dirPath string) ([]api.DirEntry, error) {
   962  	// API Documentation
   963  	// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-List%20Items%20in%20Directory
   964  	if libraryID == "" {
   965  		return nil, errors.New("cannot list files without a library")
   966  	}
   967  	dirPath = path.Join("/", dirPath)
   968  
   969  	opts := rest.Opts{
   970  		Method:     "GET",
   971  		Path:       APIv20 + libraryID + "/dir/",
   972  		Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(dirPath)}},
   973  	}
   974  	result := make([]api.DirEntry, 1)
   975  	var resp *http.Response
   976  	var err error
   977  	err = f.pacer.Call(func() (bool, error) {
   978  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   979  		return f.shouldRetry(resp, err)
   980  	})
   981  	if err != nil {
   982  		if resp != nil {
   983  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
   984  				return nil, fs.ErrorPermissionDenied
   985  			}
   986  			if resp.StatusCode == 404 {
   987  				return nil, fs.ErrorDirNotFound
   988  			}
   989  			if resp.StatusCode == 440 {
   990  				// Encrypted library and password not provided
   991  				return nil, fs.ErrorPermissionDenied
   992  			}
   993  		}
   994  		return nil, errors.Wrap(err, "failed to get directory contents")
   995  	}
   996  
   997  	// Clean up encoded names
   998  	for index, fileInfo := range result {
   999  		fileInfo.Name = f.opt.Enc.ToStandardName(fileInfo.Name)
  1000  		fileInfo.Path = f.opt.Enc.ToStandardPath(fileInfo.Path)
  1001  		result[index] = fileInfo
  1002  	}
  1003  	return result, nil
  1004  }
  1005  
  1006  func (f *Fs) copyFileAPIv2(ctx context.Context, srcLibraryID, srcPath, dstLibraryID, dstPath string) (*api.FileInfo, error) {
  1007  	// API Documentation
  1008  	// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Copy%20File
  1009  	if srcLibraryID == "" || dstLibraryID == "" {
  1010  		return nil, errors.New("libraryID and/or file path argument(s) missing")
  1011  	}
  1012  	srcPath = path.Join("/", srcPath)
  1013  	dstPath = path.Join("/", dstPath)
  1014  
  1015  	// Older API does not seem to accept JSON input here either
  1016  	postParameters := url.Values{
  1017  		"operation": {"copy"},
  1018  		"dst_repo":  {dstLibraryID},
  1019  		"dst_dir":   {f.opt.Enc.FromStandardPath(dstPath)},
  1020  	}
  1021  	opts := rest.Opts{
  1022  		Method:      "POST",
  1023  		Path:        APIv20 + srcLibraryID + "/file/",
  1024  		Parameters:  url.Values{"p": {f.opt.Enc.FromStandardPath(srcPath)}},
  1025  		ContentType: "application/x-www-form-urlencoded",
  1026  		Body:        bytes.NewBuffer([]byte(postParameters.Encode())),
  1027  	}
  1028  	result := &api.FileInfo{}
  1029  	var resp *http.Response
  1030  	var err error
  1031  	err = f.pacer.Call(func() (bool, error) {
  1032  		resp, err = f.srv.Call(ctx, &opts)
  1033  		return f.shouldRetry(resp, err)
  1034  	})
  1035  	if err != nil {
  1036  		if resp != nil {
  1037  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
  1038  				return nil, fs.ErrorPermissionDenied
  1039  			}
  1040  		}
  1041  		return nil, errors.Wrap(err, fmt.Sprintf("failed to copy file %s:'%s' to %s:'%s'", srcLibraryID, srcPath, dstLibraryID, dstPath))
  1042  	}
  1043  	err = rest.DecodeJSON(resp, &result)
  1044  	if err != nil {
  1045  		return nil, err
  1046  	}
  1047  	return f.decodeFileInfo(result), nil
  1048  }
  1049  
  1050  func (f *Fs) renameFileAPIv2(ctx context.Context, libraryID, filePath, newname string) error {
  1051  	// API Documentation
  1052  	// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Rename%20File
  1053  	if libraryID == "" || newname == "" {
  1054  		return errors.New("libraryID and/or file path argument(s) missing")
  1055  	}
  1056  	filePath = path.Join("/", filePath)
  1057  
  1058  	// No luck with JSON input with the older api2
  1059  	postParameters := url.Values{
  1060  		"operation": {"rename"},
  1061  		"reloaddir": {"true"}, // This is an undocumented trick to avoid an http code 301 response (found in https://github.com/haiwen/seahub/blob/master/seahub/api2/views.py)
  1062  		"newname":   {f.opt.Enc.FromStandardName(newname)},
  1063  	}
  1064  
  1065  	opts := rest.Opts{
  1066  		Method:      "POST",
  1067  		Path:        APIv20 + libraryID + "/file/",
  1068  		Parameters:  url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
  1069  		ContentType: "application/x-www-form-urlencoded",
  1070  		Body:        bytes.NewBuffer([]byte(postParameters.Encode())),
  1071  		NoRedirect:  true,
  1072  		NoResponse:  true,
  1073  	}
  1074  	var resp *http.Response
  1075  	var err error
  1076  	err = f.pacer.Call(func() (bool, error) {
  1077  		resp, err = f.srv.Call(ctx, &opts)
  1078  		return f.shouldRetry(resp, err)
  1079  	})
  1080  	if err != nil {
  1081  		if resp != nil {
  1082  			if resp.StatusCode == 301 {
  1083  				// This is the normal response from the server
  1084  				return nil
  1085  			}
  1086  			if resp.StatusCode == 401 || resp.StatusCode == 403 {
  1087  				return fs.ErrorPermissionDenied
  1088  			}
  1089  			if resp.StatusCode == 404 {
  1090  				return fs.ErrorObjectNotFound
  1091  			}
  1092  		}
  1093  		return errors.Wrap(err, "failed to rename file")
  1094  	}
  1095  	return nil
  1096  }