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

     1  package fichier
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/rclone/rclone/fs"
    16  	"github.com/rclone/rclone/fs/fserrors"
    17  	"github.com/rclone/rclone/lib/rest"
    18  )
    19  
    20  // retryErrorCodes is a slice of error codes that we will retry
    21  var retryErrorCodes = []int{
    22  	429, // Too Many Requests.
    23  	403, // Forbidden (may happen when request limit is exceeded)
    24  	500, // Internal Server Error
    25  	502, // Bad Gateway
    26  	503, // Service Unavailable
    27  	504, // Gateway Timeout
    28  	509, // Bandwidth Limit Exceeded
    29  }
    30  
    31  var errorRegex = regexp.MustCompile(`#(\d{1,3})`)
    32  
    33  func parseFichierError(err error) int {
    34  	matches := errorRegex.FindStringSubmatch(err.Error())
    35  	if len(matches) == 0 {
    36  		return 0
    37  	}
    38  	code, err := strconv.Atoi(matches[1])
    39  	if err != nil {
    40  		fs.Debugf(nil, "failed parsing fichier error: %v", err)
    41  		return 0
    42  	}
    43  	return code
    44  }
    45  
    46  // shouldRetry returns a boolean as to whether this resp and err
    47  // deserve to be retried.  It returns the err as a convenience
    48  func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
    49  	if fserrors.ContextError(ctx, &err) {
    50  		return false, err
    51  	}
    52  	// 1Fichier uses HTTP error code 403 (Forbidden) for all kinds of errors with
    53  	// responses looking like this: "{\"message\":\"Flood detected: IP Locked #374\",\"status\":\"KO\"}"
    54  	//
    55  	// We attempt to parse the actual 1Fichier error code from this body and handle it accordingly
    56  	// Most importantly #374 (Flood detected: IP locked) which the integration tests provoke
    57  	// The list below is far from complete and should be expanded if we see any more error codes.
    58  	if err != nil {
    59  		switch parseFichierError(err) {
    60  		case 93:
    61  			return false, err // No such user
    62  		case 186:
    63  			return false, err // IP blocked?
    64  		case 374:
    65  			fs.Debugf(nil, "Sleeping for 30 seconds due to: %v", err)
    66  			time.Sleep(30 * time.Second)
    67  		default:
    68  		}
    69  	}
    70  	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
    71  }
    72  
    73  var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString
    74  
    75  func (f *Fs) createObject(ctx context.Context, remote string) (o *Object, leaf string, directoryID string, err error) {
    76  	// Create the directory for the object if it doesn't exist
    77  	leaf, directoryID, err = f.dirCache.FindPath(ctx, remote, true)
    78  	if err != nil {
    79  		return
    80  	}
    81  	// Temporary Object under construction
    82  	o = &Object{
    83  		fs:     f,
    84  		remote: remote,
    85  	}
    86  	return o, leaf, directoryID, nil
    87  }
    88  
    89  func (f *Fs) readFileInfo(ctx context.Context, url string) (*File, error) {
    90  	request := FileInfoRequest{
    91  		URL: url,
    92  	}
    93  	opts := rest.Opts{
    94  		Method: "POST",
    95  		Path:   "/file/info.cgi",
    96  	}
    97  
    98  	var file File
    99  	err := f.pacer.Call(func() (bool, error) {
   100  		resp, err := f.rest.CallJSON(ctx, &opts, &request, &file)
   101  		return shouldRetry(ctx, resp, err)
   102  	})
   103  	if err != nil {
   104  		return nil, fmt.Errorf("couldn't read file info: %w", err)
   105  	}
   106  
   107  	return &file, err
   108  }
   109  
   110  // maybe do some actual validation later if necessary
   111  func validToken(token *GetTokenResponse) bool {
   112  	return token.Status == "OK"
   113  }
   114  
   115  func (f *Fs) getDownloadToken(ctx context.Context, url string) (*GetTokenResponse, error) {
   116  	request := DownloadRequest{
   117  		URL:    url,
   118  		Single: 1,
   119  		Pass:   f.opt.FilePassword,
   120  	}
   121  	if f.opt.CDN {
   122  		request.CDN = 1
   123  	}
   124  	opts := rest.Opts{
   125  		Method: "POST",
   126  		Path:   "/download/get_token.cgi",
   127  	}
   128  
   129  	var token GetTokenResponse
   130  	err := f.pacer.Call(func() (bool, error) {
   131  		resp, err := f.rest.CallJSON(ctx, &opts, &request, &token)
   132  		doretry, err := shouldRetry(ctx, resp, err)
   133  		return doretry || !validToken(&token), err
   134  	})
   135  	if err != nil {
   136  		return nil, fmt.Errorf("couldn't list files: %w", err)
   137  	}
   138  
   139  	return &token, nil
   140  }
   141  
   142  func fileFromSharedFile(file *SharedFile) File {
   143  	return File{
   144  		URL:      file.Link,
   145  		Filename: file.Filename,
   146  		Size:     file.Size,
   147  	}
   148  }
   149  
   150  func (f *Fs) listSharedFiles(ctx context.Context, id string) (entries fs.DirEntries, err error) {
   151  	opts := rest.Opts{
   152  		Method:      "GET",
   153  		RootURL:     "https://1fichier.com/dir/",
   154  		Path:        id,
   155  		Parameters:  map[string][]string{"json": {"1"}},
   156  		ContentType: "application/x-www-form-urlencoded",
   157  	}
   158  	if f.opt.FolderPassword != "" {
   159  		opts.Method = "POST"
   160  		opts.Parameters = nil
   161  		opts.Body = strings.NewReader("json=1&pass=" + url.QueryEscape(f.opt.FolderPassword))
   162  	}
   163  
   164  	var sharedFiles SharedFolderResponse
   165  	err = f.pacer.Call(func() (bool, error) {
   166  		resp, err := f.rest.CallJSON(ctx, &opts, nil, &sharedFiles)
   167  		return shouldRetry(ctx, resp, err)
   168  	})
   169  	if err != nil {
   170  		return nil, fmt.Errorf("couldn't list files: %w", err)
   171  	}
   172  
   173  	entries = make([]fs.DirEntry, len(sharedFiles))
   174  
   175  	for i, sharedFile := range sharedFiles {
   176  		entries[i] = f.newObjectFromFile(ctx, "", fileFromSharedFile(&sharedFile))
   177  	}
   178  
   179  	return entries, nil
   180  }
   181  
   182  func (f *Fs) listFiles(ctx context.Context, directoryID int) (filesList *FilesList, err error) {
   183  	// fs.Debugf(f, "Requesting files for dir `%s`", directoryID)
   184  	request := ListFilesRequest{
   185  		FolderID: directoryID,
   186  	}
   187  
   188  	opts := rest.Opts{
   189  		Method: "POST",
   190  		Path:   "/file/ls.cgi",
   191  	}
   192  
   193  	filesList = &FilesList{}
   194  	err = f.pacer.Call(func() (bool, error) {
   195  		resp, err := f.rest.CallJSON(ctx, &opts, &request, filesList)
   196  		return shouldRetry(ctx, resp, err)
   197  	})
   198  	if err != nil {
   199  		return nil, fmt.Errorf("couldn't list files: %w", err)
   200  	}
   201  	for i := range filesList.Items {
   202  		item := &filesList.Items[i]
   203  		item.Filename = f.opt.Enc.ToStandardName(item.Filename)
   204  	}
   205  
   206  	return filesList, nil
   207  }
   208  
   209  func (f *Fs) listFolders(ctx context.Context, directoryID int) (foldersList *FoldersList, err error) {
   210  	// fs.Debugf(f, "Requesting folders for id `%s`", directoryID)
   211  
   212  	request := ListFolderRequest{
   213  		FolderID: directoryID,
   214  	}
   215  
   216  	opts := rest.Opts{
   217  		Method: "POST",
   218  		Path:   "/folder/ls.cgi",
   219  	}
   220  
   221  	foldersList = &FoldersList{}
   222  	err = f.pacer.Call(func() (bool, error) {
   223  		resp, err := f.rest.CallJSON(ctx, &opts, &request, foldersList)
   224  		return shouldRetry(ctx, resp, err)
   225  	})
   226  	if err != nil {
   227  		return nil, fmt.Errorf("couldn't list folders: %w", err)
   228  	}
   229  	foldersList.Name = f.opt.Enc.ToStandardName(foldersList.Name)
   230  	for i := range foldersList.SubFolders {
   231  		folder := &foldersList.SubFolders[i]
   232  		folder.Name = f.opt.Enc.ToStandardName(folder.Name)
   233  	}
   234  
   235  	// fs.Debugf(f, "Got FoldersList for id `%s`", directoryID)
   236  
   237  	return foldersList, err
   238  }
   239  
   240  func (f *Fs) listDir(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   241  	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	folderID, err := strconv.Atoi(directoryID)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  
   251  	files, err := f.listFiles(ctx, folderID)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	folders, err := f.listFolders(ctx, folderID)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	entries = make([]fs.DirEntry, len(files.Items)+len(folders.SubFolders))
   262  
   263  	for i, item := range files.Items {
   264  		entries[i] = f.newObjectFromFile(ctx, dir, item)
   265  	}
   266  
   267  	for i, folder := range folders.SubFolders {
   268  		createDate, err := time.Parse("2006-01-02 15:04:05", folder.CreateDate)
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  
   273  		fullPath := getRemote(dir, folder.Name)
   274  		folderID := strconv.Itoa(folder.ID)
   275  
   276  		entries[len(files.Items)+i] = fs.NewDir(fullPath, createDate).SetID(folderID)
   277  
   278  		// fs.Debugf(f, "Put Path `%s` for id `%d` into dircache", fullPath, folder.ID)
   279  		f.dirCache.Put(fullPath, folderID)
   280  	}
   281  
   282  	return entries, nil
   283  }
   284  
   285  func (f *Fs) newObjectFromFile(ctx context.Context, dir string, item File) *Object {
   286  	return &Object{
   287  		fs:     f,
   288  		remote: getRemote(dir, item.Filename),
   289  		file:   item,
   290  	}
   291  }
   292  
   293  func getRemote(dir, fileName string) string {
   294  	if dir == "" {
   295  		return fileName
   296  	}
   297  
   298  	return dir + "/" + fileName
   299  }
   300  
   301  func (f *Fs) makeFolder(ctx context.Context, leaf string, folderID int) (response *MakeFolderResponse, err error) {
   302  	name := f.opt.Enc.FromStandardName(leaf)
   303  	// fs.Debugf(f, "Creating folder `%s` in id `%s`", name, directoryID)
   304  
   305  	request := MakeFolderRequest{
   306  		FolderID: folderID,
   307  		Name:     name,
   308  	}
   309  
   310  	opts := rest.Opts{
   311  		Method: "POST",
   312  		Path:   "/folder/mkdir.cgi",
   313  	}
   314  
   315  	response = &MakeFolderResponse{}
   316  	err = f.pacer.Call(func() (bool, error) {
   317  		resp, err := f.rest.CallJSON(ctx, &opts, &request, response)
   318  		return shouldRetry(ctx, resp, err)
   319  	})
   320  	if err != nil {
   321  		return nil, fmt.Errorf("couldn't create folder: %w", err)
   322  	}
   323  
   324  	// fs.Debugf(f, "Created Folder `%s` in id `%s`", name, directoryID)
   325  
   326  	return response, err
   327  }
   328  
   329  func (f *Fs) removeFolder(ctx context.Context, name string, folderID int) (response *GenericOKResponse, err error) {
   330  	// fs.Debugf(f, "Removing folder with id `%s`", directoryID)
   331  
   332  	request := &RemoveFolderRequest{
   333  		FolderID: folderID,
   334  	}
   335  
   336  	opts := rest.Opts{
   337  		Method: "POST",
   338  		Path:   "/folder/rm.cgi",
   339  	}
   340  
   341  	response = &GenericOKResponse{}
   342  	var resp *http.Response
   343  	err = f.pacer.Call(func() (bool, error) {
   344  		resp, err = f.rest.CallJSON(ctx, &opts, request, response)
   345  		return shouldRetry(ctx, resp, err)
   346  	})
   347  	if err != nil {
   348  		return nil, fmt.Errorf("couldn't remove folder: %w", err)
   349  	}
   350  	if response.Status != "OK" {
   351  		return nil, fmt.Errorf("can't remove folder: %s", response.Message)
   352  	}
   353  
   354  	// fs.Debugf(f, "Removed Folder with id `%s`", directoryID)
   355  
   356  	return response, nil
   357  }
   358  
   359  func (f *Fs) deleteFile(ctx context.Context, url string) (response *GenericOKResponse, err error) {
   360  	request := &RemoveFileRequest{
   361  		Files: []RmFile{
   362  			{url},
   363  		},
   364  	}
   365  
   366  	opts := rest.Opts{
   367  		Method: "POST",
   368  		Path:   "/file/rm.cgi",
   369  	}
   370  
   371  	response = &GenericOKResponse{}
   372  	err = f.pacer.Call(func() (bool, error) {
   373  		resp, err := f.rest.CallJSON(ctx, &opts, request, response)
   374  		return shouldRetry(ctx, resp, err)
   375  	})
   376  
   377  	if err != nil {
   378  		return nil, fmt.Errorf("couldn't remove file: %w", err)
   379  	}
   380  
   381  	// fs.Debugf(f, "Removed file with url `%s`", url)
   382  
   383  	return response, nil
   384  }
   385  
   386  func (f *Fs) moveFile(ctx context.Context, url string, folderID int, rename string) (response *MoveFileResponse, err error) {
   387  	request := &MoveFileRequest{
   388  		URLs:     []string{url},
   389  		FolderID: folderID,
   390  		Rename:   rename,
   391  	}
   392  
   393  	opts := rest.Opts{
   394  		Method: "POST",
   395  		Path:   "/file/mv.cgi",
   396  	}
   397  
   398  	response = &MoveFileResponse{}
   399  	err = f.pacer.Call(func() (bool, error) {
   400  		resp, err := f.rest.CallJSON(ctx, &opts, request, response)
   401  		return shouldRetry(ctx, resp, err)
   402  	})
   403  
   404  	if err != nil {
   405  		return nil, fmt.Errorf("couldn't copy file: %w", err)
   406  	}
   407  
   408  	return response, nil
   409  }
   410  
   411  func (f *Fs) moveDir(ctx context.Context, folderID int, newLeaf string, destinationFolderID int) (response *MoveDirResponse, err error) {
   412  	request := &MoveDirRequest{
   413  		FolderID:            folderID,
   414  		DestinationFolderID: destinationFolderID,
   415  		Rename:              newLeaf,
   416  		// DestinationUser:     destinationUser,
   417  	}
   418  
   419  	opts := rest.Opts{
   420  		Method: "POST",
   421  		Path:   "/folder/mv.cgi",
   422  	}
   423  
   424  	response = &MoveDirResponse{}
   425  	err = f.pacer.Call(func() (bool, error) {
   426  		resp, err := f.rest.CallJSON(ctx, &opts, request, response)
   427  		return shouldRetry(ctx, resp, err)
   428  	})
   429  
   430  	if err != nil {
   431  		return nil, fmt.Errorf("couldn't move dir: %w", err)
   432  	}
   433  
   434  	return response, nil
   435  }
   436  
   437  func (f *Fs) copyFile(ctx context.Context, url string, folderID int, rename string) (response *CopyFileResponse, err error) {
   438  	request := &CopyFileRequest{
   439  		URLs:     []string{url},
   440  		FolderID: folderID,
   441  		Rename:   rename,
   442  	}
   443  
   444  	opts := rest.Opts{
   445  		Method: "POST",
   446  		Path:   "/file/cp.cgi",
   447  	}
   448  
   449  	response = &CopyFileResponse{}
   450  	err = f.pacer.Call(func() (bool, error) {
   451  		resp, err := f.rest.CallJSON(ctx, &opts, request, response)
   452  		return shouldRetry(ctx, resp, err)
   453  	})
   454  
   455  	if err != nil {
   456  		return nil, fmt.Errorf("couldn't copy file: %w", err)
   457  	}
   458  
   459  	return response, nil
   460  }
   461  
   462  func (f *Fs) renameFile(ctx context.Context, url string, newName string) (response *RenameFileResponse, err error) {
   463  	request := &RenameFileRequest{
   464  		URLs: []RenameFileURL{
   465  			{
   466  				URL:      url,
   467  				Filename: newName,
   468  			},
   469  		},
   470  	}
   471  
   472  	opts := rest.Opts{
   473  		Method: "POST",
   474  		Path:   "/file/rename.cgi",
   475  	}
   476  
   477  	response = &RenameFileResponse{}
   478  	err = f.pacer.Call(func() (bool, error) {
   479  		resp, err := f.rest.CallJSON(ctx, &opts, request, response)
   480  		return shouldRetry(ctx, resp, err)
   481  	})
   482  
   483  	if err != nil {
   484  		return nil, fmt.Errorf("couldn't rename file: %w", err)
   485  	}
   486  
   487  	return response, nil
   488  }
   489  
   490  func (f *Fs) getUploadNode(ctx context.Context) (response *GetUploadNodeResponse, err error) {
   491  	// fs.Debugf(f, "Requesting Upload node")
   492  
   493  	opts := rest.Opts{
   494  		Method:      "GET",
   495  		ContentType: "application/json", // 1Fichier API is bad
   496  		Path:        "/upload/get_upload_server.cgi",
   497  	}
   498  
   499  	response = &GetUploadNodeResponse{}
   500  	err = f.pacer.Call(func() (bool, error) {
   501  		resp, err := f.rest.CallJSON(ctx, &opts, nil, response)
   502  		return shouldRetry(ctx, resp, err)
   503  	})
   504  	if err != nil {
   505  		return nil, fmt.Errorf("didn't get an upload node: %w", err)
   506  	}
   507  
   508  	// fs.Debugf(f, "Got Upload node")
   509  
   510  	return response, err
   511  }
   512  
   513  func (f *Fs) uploadFile(ctx context.Context, in io.Reader, size int64, fileName, folderID, uploadID, node string, options ...fs.OpenOption) (response *http.Response, err error) {
   514  	// fs.Debugf(f, "Uploading File `%s`", fileName)
   515  
   516  	fileName = f.opt.Enc.FromStandardName(fileName)
   517  
   518  	if len(uploadID) > 10 || !isAlphaNumeric(uploadID) {
   519  		return nil, errors.New("invalid UploadID")
   520  	}
   521  
   522  	opts := rest.Opts{
   523  		Method: "POST",
   524  		Path:   "/upload.cgi",
   525  		Parameters: map[string][]string{
   526  			"id": {uploadID},
   527  		},
   528  		NoResponse:           true,
   529  		Body:                 in,
   530  		ContentLength:        &size,
   531  		Options:              options,
   532  		MultipartContentName: "file[]",
   533  		MultipartFileName:    fileName,
   534  		MultipartParams: map[string][]string{
   535  			"did": {folderID},
   536  		},
   537  	}
   538  
   539  	if node != "" {
   540  		opts.RootURL = "https://" + node
   541  	}
   542  
   543  	err = f.pacer.CallNoRetry(func() (bool, error) {
   544  		resp, err := f.rest.CallJSON(ctx, &opts, nil, nil)
   545  		return shouldRetry(ctx, resp, err)
   546  	})
   547  
   548  	if err != nil {
   549  		return nil, fmt.Errorf("couldn't upload file: %w", err)
   550  	}
   551  
   552  	// fs.Debugf(f, "Uploaded File `%s`", fileName)
   553  
   554  	return response, err
   555  }
   556  
   557  func (f *Fs) endUpload(ctx context.Context, uploadID string, nodeurl string) (response *EndFileUploadResponse, err error) {
   558  	// fs.Debugf(f, "Ending File Upload `%s`", uploadID)
   559  
   560  	if len(uploadID) > 10 || !isAlphaNumeric(uploadID) {
   561  		return nil, errors.New("invalid UploadID")
   562  	}
   563  
   564  	opts := rest.Opts{
   565  		Method:  "GET",
   566  		Path:    "/end.pl",
   567  		RootURL: "https://" + nodeurl,
   568  		Parameters: map[string][]string{
   569  			"xid": {uploadID},
   570  		},
   571  		ExtraHeaders: map[string]string{
   572  			"JSON": "1",
   573  		},
   574  	}
   575  
   576  	response = &EndFileUploadResponse{}
   577  	err = f.pacer.Call(func() (bool, error) {
   578  		resp, err := f.rest.CallJSON(ctx, &opts, nil, response)
   579  		return shouldRetry(ctx, resp, err)
   580  	})
   581  
   582  	if err != nil {
   583  		return nil, fmt.Errorf("couldn't finish file upload: %w", err)
   584  	}
   585  
   586  	return response, err
   587  }