github.com/artpar/rclone@v1.67.3/backend/seafile/webapi.go (about)

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