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

     1  // Package fichier provides an interface to the 1Fichier storage system.
     2  package fichier
     3  
     4  import (
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/rclone/rclone/fs"
    15  	"github.com/rclone/rclone/fs/config"
    16  	"github.com/rclone/rclone/fs/config/configmap"
    17  	"github.com/rclone/rclone/fs/config/configstruct"
    18  	"github.com/rclone/rclone/fs/fshttp"
    19  	"github.com/rclone/rclone/fs/hash"
    20  	"github.com/rclone/rclone/lib/dircache"
    21  	"github.com/rclone/rclone/lib/encoder"
    22  	"github.com/rclone/rclone/lib/pacer"
    23  	"github.com/rclone/rclone/lib/rest"
    24  )
    25  
    26  const (
    27  	rootID         = "0"
    28  	apiBaseURL     = "https://api.1fichier.com/v1"
    29  	minSleep       = 400 * time.Millisecond // api is extremely rate limited now
    30  	maxSleep       = 5 * time.Second
    31  	decayConstant  = 2 // bigger for slower decay, exponential
    32  	attackConstant = 0 // start with max sleep
    33  )
    34  
    35  func init() {
    36  	fs.Register(&fs.RegInfo{
    37  		Name:        "fichier",
    38  		Description: "1Fichier",
    39  		NewFs:       NewFs,
    40  		Options: []fs.Option{{
    41  			Help:      "Your API Key, get it from https://1fichier.com/console/params.pl.",
    42  			Name:      "api_key",
    43  			Sensitive: true,
    44  		}, {
    45  			Help:     "If you want to download a shared folder, add this parameter.",
    46  			Name:     "shared_folder",
    47  			Advanced: true,
    48  		}, {
    49  			Help:       "If you want to download a shared file that is password protected, add this parameter.",
    50  			Name:       "file_password",
    51  			Advanced:   true,
    52  			IsPassword: true,
    53  		}, {
    54  			Help:       "If you want to list the files in a shared folder that is password protected, add this parameter.",
    55  			Name:       "folder_password",
    56  			Advanced:   true,
    57  			IsPassword: true,
    58  		}, {
    59  			Help:     "Set if you wish to use CDN download links.",
    60  			Name:     "cdn",
    61  			Default:  false,
    62  			Advanced: true,
    63  		}, {
    64  			Name:     config.ConfigEncoding,
    65  			Help:     config.ConfigEncodingHelp,
    66  			Advanced: true,
    67  			// Characters that need escaping
    68  			//
    69  			// 		'\\': '\', // FULLWIDTH REVERSE SOLIDUS
    70  			// 		'<':  '<', // FULLWIDTH LESS-THAN SIGN
    71  			// 		'>':  '>', // FULLWIDTH GREATER-THAN SIGN
    72  			// 		'"':  '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved
    73  			// 		'\'': ''', // FULLWIDTH APOSTROPHE
    74  			// 		'$':  '$', // FULLWIDTH DOLLAR SIGN
    75  			// 		'`':  '`', // FULLWIDTH GRAVE ACCENT
    76  			//
    77  			// Leading space and trailing space
    78  			Default: (encoder.Display |
    79  				encoder.EncodeBackSlash |
    80  				encoder.EncodeSingleQuote |
    81  				encoder.EncodeBackQuote |
    82  				encoder.EncodeDoubleQuote |
    83  				encoder.EncodeLtGt |
    84  				encoder.EncodeDollar |
    85  				encoder.EncodeLeftSpace |
    86  				encoder.EncodeRightSpace |
    87  				encoder.EncodeInvalidUtf8),
    88  		}},
    89  	})
    90  }
    91  
    92  // Options defines the configuration for this backend
    93  type Options struct {
    94  	APIKey         string               `config:"api_key"`
    95  	SharedFolder   string               `config:"shared_folder"`
    96  	FilePassword   string               `config:"file_password"`
    97  	FolderPassword string               `config:"folder_password"`
    98  	CDN            bool                 `config:"cdn"`
    99  	Enc            encoder.MultiEncoder `config:"encoding"`
   100  }
   101  
   102  // Fs is the interface a cloud storage system must provide
   103  type Fs struct {
   104  	root       string
   105  	name       string
   106  	features   *fs.Features
   107  	opt        Options
   108  	dirCache   *dircache.DirCache
   109  	baseClient *http.Client
   110  	pacer      *fs.Pacer
   111  	rest       *rest.Client
   112  }
   113  
   114  // FindLeaf finds a directory of name leaf in the folder with ID pathID
   115  func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
   116  	folderID, err := strconv.Atoi(pathID)
   117  	if err != nil {
   118  		return "", false, err
   119  	}
   120  	folders, err := f.listFolders(ctx, folderID)
   121  	if err != nil {
   122  		return "", false, err
   123  	}
   124  
   125  	for _, folder := range folders.SubFolders {
   126  		if folder.Name == leaf {
   127  			pathIDOut := strconv.Itoa(folder.ID)
   128  			return pathIDOut, true, nil
   129  		}
   130  	}
   131  
   132  	return "", false, nil
   133  }
   134  
   135  // CreateDir makes a directory with pathID as parent and name leaf
   136  func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) {
   137  	folderID, err := strconv.Atoi(pathID)
   138  	if err != nil {
   139  		return "", err
   140  	}
   141  	resp, err := f.makeFolder(ctx, leaf, folderID)
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  	return strconv.Itoa(resp.FolderID), err
   146  }
   147  
   148  // Name of the remote (as passed into NewFs)
   149  func (f *Fs) Name() string {
   150  	return f.name
   151  }
   152  
   153  // Root of the remote (as passed into NewFs)
   154  func (f *Fs) Root() string {
   155  	return f.root
   156  }
   157  
   158  // String returns a description of the FS
   159  func (f *Fs) String() string {
   160  	return fmt.Sprintf("1Fichier root '%s'", f.root)
   161  }
   162  
   163  // Precision of the ModTimes in this Fs
   164  func (f *Fs) Precision() time.Duration {
   165  	return fs.ModTimeNotSupported
   166  }
   167  
   168  // Hashes returns the supported hash types of the filesystem
   169  func (f *Fs) Hashes() hash.Set {
   170  	return hash.Set(hash.Whirlpool)
   171  }
   172  
   173  // Features returns the optional features of this Fs
   174  func (f *Fs) Features() *fs.Features {
   175  	return f.features
   176  }
   177  
   178  // NewFs makes a new Fs object from the path
   179  //
   180  // The path is of the form remote:path
   181  //
   182  // Remotes are looked up in the config file.  If the remote isn't
   183  // found then NotFoundInConfigFile will be returned.
   184  //
   185  // On Windows avoid single character remote names as they can be mixed
   186  // up with drive letters.
   187  func NewFs(ctx context.Context, name string, root string, config configmap.Mapper) (fs.Fs, error) {
   188  	opt := new(Options)
   189  	err := configstruct.Set(config, opt)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	// If using a Shared Folder override root
   195  	if opt.SharedFolder != "" {
   196  		root = ""
   197  	}
   198  
   199  	//workaround for wonky parser
   200  	root = strings.Trim(root, "/")
   201  
   202  	f := &Fs{
   203  		name:       name,
   204  		root:       root,
   205  		opt:        *opt,
   206  		pacer:      fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant), pacer.AttackConstant(attackConstant))),
   207  		baseClient: &http.Client{},
   208  	}
   209  
   210  	f.features = (&fs.Features{
   211  		DuplicateFiles:          true,
   212  		CanHaveEmptyDirectories: true,
   213  		ReadMimeType:            true,
   214  	}).Fill(ctx, f)
   215  
   216  	client := fshttp.NewClient(ctx)
   217  
   218  	f.rest = rest.NewClient(client).SetRoot(apiBaseURL)
   219  
   220  	f.rest.SetHeader("Authorization", "Bearer "+f.opt.APIKey)
   221  
   222  	f.dirCache = dircache.New(root, rootID, f)
   223  
   224  	// Find the current root
   225  	err = f.dirCache.FindRoot(ctx, false)
   226  	if err != nil {
   227  		// Assume it is a file
   228  		newRoot, remote := dircache.SplitPath(root)
   229  		tempF := *f
   230  		tempF.dirCache = dircache.New(newRoot, rootID, &tempF)
   231  		tempF.root = newRoot
   232  		// Make new Fs which is the parent
   233  		err = tempF.dirCache.FindRoot(ctx, false)
   234  		if err != nil {
   235  			// No root so return old f
   236  			return f, nil
   237  		}
   238  		_, err := tempF.NewObject(ctx, remote)
   239  		if err != nil {
   240  			if err == fs.ErrorObjectNotFound {
   241  				// File doesn't exist so return old f
   242  				return f, nil
   243  			}
   244  			return nil, err
   245  		}
   246  		f.features.Fill(ctx, &tempF)
   247  		// XXX: update the old f here instead of returning tempF, since
   248  		// `features` were already filled with functions having *f as a receiver.
   249  		// See https://github.com/rclone/rclone/issues/2182
   250  		f.dirCache = tempF.dirCache
   251  		f.root = tempF.root
   252  		// return an error with an fs which points to the parent
   253  		return f, fs.ErrorIsFile
   254  	}
   255  	return f, nil
   256  }
   257  
   258  // List the objects and directories in dir into entries.  The
   259  // entries can be returned in any order but should be for a
   260  // complete directory.
   261  //
   262  // dir should be "" to list the root, and should not have
   263  // trailing slashes.
   264  //
   265  // This should return ErrDirNotFound if the directory isn't
   266  // found.
   267  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   268  	if f.opt.SharedFolder != "" {
   269  		return f.listSharedFiles(ctx, f.opt.SharedFolder)
   270  	}
   271  
   272  	dirContent, err := f.listDir(ctx, dir)
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  
   277  	return dirContent, nil
   278  }
   279  
   280  // NewObject finds the Object at remote.  If it can't be found
   281  // it returns the error ErrorObjectNotFound.
   282  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   283  	leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, false)
   284  	if err != nil {
   285  		if err == fs.ErrorDirNotFound {
   286  			return nil, fs.ErrorObjectNotFound
   287  		}
   288  		return nil, err
   289  	}
   290  
   291  	folderID, err := strconv.Atoi(directoryID)
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  	files, err := f.listFiles(ctx, folderID)
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  
   300  	for _, file := range files.Items {
   301  		if file.Filename == leaf {
   302  			path, ok := f.dirCache.GetInv(directoryID)
   303  
   304  			if !ok {
   305  				return nil, errors.New("cannot find dir in dircache")
   306  			}
   307  
   308  			return f.newObjectFromFile(ctx, path, file), nil
   309  		}
   310  	}
   311  
   312  	return nil, fs.ErrorObjectNotFound
   313  }
   314  
   315  // Put in to the remote path with the modTime given of the given size
   316  //
   317  // When called from outside an Fs by rclone, src.Size() will always be >= 0.
   318  // But for unknown-sized objects (indicated by src.Size() == -1), Put should either
   319  // return an error or upload it properly (rather than e.g. calling panic).
   320  //
   321  // May create the object even if it returns an error - if so
   322  // will return the object and the error, otherwise will return
   323  // nil and the error
   324  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   325  	existingObj, err := f.NewObject(ctx, src.Remote())
   326  	switch err {
   327  	case nil:
   328  		return existingObj, existingObj.Update(ctx, in, src, options...)
   329  	case fs.ErrorObjectNotFound:
   330  		// Not found so create it
   331  		return f.PutUnchecked(ctx, in, src, options...)
   332  	default:
   333  		return nil, err
   334  	}
   335  }
   336  
   337  // putUnchecked uploads the object with the given name and size
   338  //
   339  // This will create a duplicate if we upload a new file without
   340  // checking to see if there is one already - use Put() for that.
   341  func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size int64, options ...fs.OpenOption) (fs.Object, error) {
   342  	if size > int64(300e9) {
   343  		return nil, errors.New("File too big, can't upload")
   344  	} else if size == 0 {
   345  		return nil, fs.ErrorCantUploadEmptyFiles
   346  	}
   347  
   348  	nodeResponse, err := f.getUploadNode(ctx)
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  
   353  	leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, true)
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  
   358  	_, err = f.uploadFile(ctx, in, size, leaf, directoryID, nodeResponse.ID, nodeResponse.URL, options...)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	fileUploadResponse, err := f.endUpload(ctx, nodeResponse.ID, nodeResponse.URL)
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  
   368  	if len(fileUploadResponse.Links) == 0 {
   369  		return nil, errors.New("upload response not found")
   370  	} else if len(fileUploadResponse.Links) > 1 {
   371  		fs.Debugf(remote, "Multiple upload responses found, using the first")
   372  	}
   373  
   374  	link := fileUploadResponse.Links[0]
   375  	fileSize, err := strconv.ParseInt(link.Size, 10, 64)
   376  
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  
   381  	return &Object{
   382  		fs:     f,
   383  		remote: remote,
   384  		file: File{
   385  			CDN:         0,
   386  			Checksum:    link.Whirlpool,
   387  			ContentType: "",
   388  			Date:        time.Now().Format("2006-01-02 15:04:05"),
   389  			Filename:    link.Filename,
   390  			Pass:        0,
   391  			Size:        fileSize,
   392  			URL:         link.Download,
   393  		},
   394  	}, nil
   395  }
   396  
   397  // PutUnchecked uploads the object
   398  //
   399  // This will create a duplicate if we upload a new file without
   400  // checking to see if there is one already - use Put() for that.
   401  func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   402  	return f.putUnchecked(ctx, in, src.Remote(), src.Size(), options...)
   403  }
   404  
   405  // Mkdir makes the directory (container, bucket)
   406  //
   407  // Shouldn't return an error if it already exists
   408  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   409  	_, err := f.dirCache.FindDir(ctx, dir, true)
   410  	return err
   411  }
   412  
   413  // Rmdir removes the directory (container, bucket) if empty
   414  //
   415  // Return an error if it doesn't exist or isn't empty
   416  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   417  	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
   418  	if err != nil {
   419  		return err
   420  	}
   421  
   422  	folderID, err := strconv.Atoi(directoryID)
   423  	if err != nil {
   424  		return err
   425  	}
   426  
   427  	_, err = f.removeFolder(ctx, dir, folderID)
   428  	if err != nil {
   429  		return err
   430  	}
   431  
   432  	f.dirCache.FlushDir(dir)
   433  
   434  	return nil
   435  }
   436  
   437  // Move src to this remote using server side move operations.
   438  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   439  	srcObj, ok := src.(*Object)
   440  	if !ok {
   441  		fs.Debugf(src, "Can't move - not same remote type")
   442  		return nil, fs.ErrorCantMove
   443  	}
   444  
   445  	// Find current directory ID
   446  	_, currentDirectoryID, err := f.dirCache.FindPath(ctx, remote, false)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  
   451  	// Create temporary object
   452  	dstObj, leaf, directoryID, err := f.createObject(ctx, remote)
   453  	if err != nil {
   454  		return nil, err
   455  	}
   456  
   457  	// If it is in the correct directory, just rename it
   458  	var url string
   459  	if currentDirectoryID == directoryID {
   460  		resp, err := f.renameFile(ctx, srcObj.file.URL, leaf)
   461  		if err != nil {
   462  			return nil, fmt.Errorf("couldn't rename file: %w", err)
   463  		}
   464  		if resp.Status != "OK" {
   465  			return nil, fmt.Errorf("couldn't rename file: %s", resp.Message)
   466  		}
   467  		url = resp.URLs[0].URL
   468  	} else {
   469  		folderID, err := strconv.Atoi(directoryID)
   470  		if err != nil {
   471  			return nil, err
   472  		}
   473  		resp, err := f.moveFile(ctx, srcObj.file.URL, folderID, leaf)
   474  		if err != nil {
   475  			return nil, fmt.Errorf("couldn't move file: %w", err)
   476  		}
   477  		if resp.Status != "OK" {
   478  			return nil, fmt.Errorf("couldn't move file: %s", resp.Message)
   479  		}
   480  		url = resp.URLs[0]
   481  	}
   482  
   483  	file, err := f.readFileInfo(ctx, url)
   484  	if err != nil {
   485  		return nil, errors.New("couldn't read file data")
   486  	}
   487  	dstObj.setMetaData(*file)
   488  	return dstObj, nil
   489  }
   490  
   491  // DirMove moves src, srcRemote to this remote at dstRemote
   492  // using server-side move operations.
   493  //
   494  // Will only be called if src.Fs().Name() == f.Name()
   495  //
   496  // If it isn't possible then return fs.ErrorCantDirMove.
   497  //
   498  // If destination exists then return fs.ErrorDirExists.
   499  //
   500  // This is complicated by the fact that we can't use moveDir to move
   501  // to a different directory AND rename at the same time as it can
   502  // overwrite files in the source directory.
   503  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
   504  	srcFs, ok := src.(*Fs)
   505  	if !ok {
   506  		fs.Debugf(srcFs, "Can't move directory - not same remote type")
   507  		return fs.ErrorCantDirMove
   508  	}
   509  
   510  	srcID, _, _, dstDirectoryID, dstLeaf, err := f.dirCache.DirMove(ctx, srcFs.dirCache, srcFs.root, srcRemote, f.root, dstRemote)
   511  	if err != nil {
   512  		return err
   513  	}
   514  	srcIDnumeric, err := strconv.Atoi(srcID)
   515  	if err != nil {
   516  		return err
   517  	}
   518  	dstDirectoryIDnumeric, err := strconv.Atoi(dstDirectoryID)
   519  	if err != nil {
   520  		return err
   521  	}
   522  
   523  	var resp *MoveDirResponse
   524  	resp, err = f.moveDir(ctx, srcIDnumeric, dstLeaf, dstDirectoryIDnumeric)
   525  	if err != nil {
   526  		return fmt.Errorf("couldn't rename leaf: %w", err)
   527  	}
   528  	if resp.Status != "OK" {
   529  		return fmt.Errorf("couldn't rename leaf: %s", resp.Message)
   530  	}
   531  
   532  	srcFs.dirCache.FlushDir(srcRemote)
   533  	return nil
   534  }
   535  
   536  // Copy src to this remote using server side move operations.
   537  func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   538  	srcObj, ok := src.(*Object)
   539  	if !ok {
   540  		fs.Debugf(src, "Can't move - not same remote type")
   541  		return nil, fs.ErrorCantMove
   542  	}
   543  
   544  	// Create temporary object
   545  	dstObj, leaf, directoryID, err := f.createObject(ctx, remote)
   546  	if err != nil {
   547  		return nil, err
   548  	}
   549  
   550  	folderID, err := strconv.Atoi(directoryID)
   551  	if err != nil {
   552  		return nil, err
   553  	}
   554  	resp, err := f.copyFile(ctx, srcObj.file.URL, folderID, leaf)
   555  	if err != nil {
   556  		return nil, fmt.Errorf("couldn't move file: %w", err)
   557  	}
   558  	if resp.Status != "OK" {
   559  		return nil, fmt.Errorf("couldn't move file: %s", resp.Message)
   560  	}
   561  
   562  	file, err := f.readFileInfo(ctx, resp.URLs[0].ToURL)
   563  	if err != nil {
   564  		return nil, errors.New("couldn't read file data")
   565  	}
   566  	dstObj.setMetaData(*file)
   567  	return dstObj, nil
   568  }
   569  
   570  // About gets quota information
   571  func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
   572  	opts := rest.Opts{
   573  		Method:      "POST",
   574  		Path:        "/user/info.cgi",
   575  		ContentType: "application/json",
   576  	}
   577  	var accountInfo AccountInfo
   578  	var resp *http.Response
   579  	err = f.pacer.Call(func() (bool, error) {
   580  		resp, err = f.rest.CallJSON(ctx, &opts, nil, &accountInfo)
   581  		return shouldRetry(ctx, resp, err)
   582  	})
   583  	if err != nil {
   584  		return nil, fmt.Errorf("failed to read user info: %w", err)
   585  	}
   586  
   587  	// FIXME max upload size would be useful to use in Update
   588  	usage = &fs.Usage{
   589  		Used:  fs.NewUsageValue(accountInfo.ColdStorage),                                    // bytes in use
   590  		Total: fs.NewUsageValue(accountInfo.AvailableColdStorage),                           // bytes total
   591  		Free:  fs.NewUsageValue(accountInfo.AvailableColdStorage - accountInfo.ColdStorage), // bytes free
   592  	}
   593  	return usage, nil
   594  }
   595  
   596  // PublicLink adds a "readable by anyone with link" permission on the given file or folder.
   597  func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
   598  	o, err := f.NewObject(ctx, remote)
   599  	if err != nil {
   600  		return "", err
   601  	}
   602  	return o.(*Object).file.URL, nil
   603  }
   604  
   605  // Check the interfaces are satisfied
   606  var (
   607  	_ fs.Fs              = (*Fs)(nil)
   608  	_ fs.Mover           = (*Fs)(nil)
   609  	_ fs.DirMover        = (*Fs)(nil)
   610  	_ fs.Copier          = (*Fs)(nil)
   611  	_ fs.PublicLinker    = (*Fs)(nil)
   612  	_ fs.PutUncheckeder  = (*Fs)(nil)
   613  	_ dircache.DirCacher = (*Fs)(nil)
   614  )