github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/backend/fichier/fichier.go (about)

     1  package fichier
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/pkg/errors"
    13  	"github.com/rclone/rclone/fs"
    14  	"github.com/rclone/rclone/fs/config"
    15  	"github.com/rclone/rclone/fs/config/configmap"
    16  	"github.com/rclone/rclone/fs/config/configstruct"
    17  	"github.com/rclone/rclone/fs/fshttp"
    18  	"github.com/rclone/rclone/fs/hash"
    19  	"github.com/rclone/rclone/lib/dircache"
    20  	"github.com/rclone/rclone/lib/encoder"
    21  	"github.com/rclone/rclone/lib/pacer"
    22  	"github.com/rclone/rclone/lib/rest"
    23  )
    24  
    25  const (
    26  	rootID        = "0"
    27  	apiBaseURL    = "https://api.1fichier.com/v1"
    28  	minSleep      = 334 * time.Millisecond // 3 API calls per second is recommended
    29  	maxSleep      = 5 * time.Second
    30  	decayConstant = 2 // bigger for slower decay, exponential
    31  )
    32  
    33  func init() {
    34  	fs.Register(&fs.RegInfo{
    35  		Name:        "fichier",
    36  		Description: "1Fichier",
    37  		Config: func(name string, config configmap.Mapper) {
    38  		},
    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  		}, {
    44  			Help:     "If you want to download a shared folder, add this parameter",
    45  			Name:     "shared_folder",
    46  			Required: false,
    47  			Advanced: true,
    48  		}, {
    49  			Name:     config.ConfigEncoding,
    50  			Help:     config.ConfigEncodingHelp,
    51  			Advanced: true,
    52  			// Characters that need escaping
    53  			//
    54  			// 		'\\': '\', // FULLWIDTH REVERSE SOLIDUS
    55  			// 		'<':  '<', // FULLWIDTH LESS-THAN SIGN
    56  			// 		'>':  '>', // FULLWIDTH GREATER-THAN SIGN
    57  			// 		'"':  '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved
    58  			// 		'\'': ''', // FULLWIDTH APOSTROPHE
    59  			// 		'$':  '$', // FULLWIDTH DOLLAR SIGN
    60  			// 		'`':  '`', // FULLWIDTH GRAVE ACCENT
    61  			//
    62  			// Leading space and trailing space
    63  			Default: (encoder.Display |
    64  				encoder.EncodeBackSlash |
    65  				encoder.EncodeSingleQuote |
    66  				encoder.EncodeBackQuote |
    67  				encoder.EncodeDoubleQuote |
    68  				encoder.EncodeLtGt |
    69  				encoder.EncodeDollar |
    70  				encoder.EncodeLeftSpace |
    71  				encoder.EncodeRightSpace |
    72  				encoder.EncodeInvalidUtf8),
    73  		}},
    74  	})
    75  }
    76  
    77  // Options defines the configuration for this backend
    78  type Options struct {
    79  	APIKey       string               `config:"api_key"`
    80  	SharedFolder string               `config:"shared_folder"`
    81  	Enc          encoder.MultiEncoder `config:"encoding"`
    82  }
    83  
    84  // Fs is the interface a cloud storage system must provide
    85  type Fs struct {
    86  	root       string
    87  	name       string
    88  	features   *fs.Features
    89  	opt        Options
    90  	dirCache   *dircache.DirCache
    91  	baseClient *http.Client
    92  	pacer      *fs.Pacer
    93  	rest       *rest.Client
    94  }
    95  
    96  // FindLeaf finds a directory of name leaf in the folder with ID pathID
    97  func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
    98  	folderID, err := strconv.Atoi(pathID)
    99  	if err != nil {
   100  		return "", false, err
   101  	}
   102  	folders, err := f.listFolders(ctx, folderID)
   103  	if err != nil {
   104  		return "", false, err
   105  	}
   106  
   107  	for _, folder := range folders.SubFolders {
   108  		if folder.Name == leaf {
   109  			pathIDOut := strconv.Itoa(folder.ID)
   110  			return pathIDOut, true, nil
   111  		}
   112  	}
   113  
   114  	return "", false, nil
   115  }
   116  
   117  // CreateDir makes a directory with pathID as parent and name leaf
   118  func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) {
   119  	folderID, err := strconv.Atoi(pathID)
   120  	if err != nil {
   121  		return "", err
   122  	}
   123  	resp, err := f.makeFolder(ctx, leaf, folderID)
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  	return strconv.Itoa(resp.FolderID), err
   128  }
   129  
   130  // Name of the remote (as passed into NewFs)
   131  func (f *Fs) Name() string {
   132  	return f.name
   133  }
   134  
   135  // Root of the remote (as passed into NewFs)
   136  func (f *Fs) Root() string {
   137  	return f.root
   138  }
   139  
   140  // String returns a description of the FS
   141  func (f *Fs) String() string {
   142  	return fmt.Sprintf("1Fichier root '%s'", f.root)
   143  }
   144  
   145  // Precision of the ModTimes in this Fs
   146  func (f *Fs) Precision() time.Duration {
   147  	return fs.ModTimeNotSupported
   148  }
   149  
   150  // Hashes returns the supported hash types of the filesystem
   151  func (f *Fs) Hashes() hash.Set {
   152  	return hash.Set(hash.Whirlpool)
   153  }
   154  
   155  // Features returns the optional features of this Fs
   156  func (f *Fs) Features() *fs.Features {
   157  	return f.features
   158  }
   159  
   160  // NewFs makes a new Fs object from the path
   161  //
   162  // The path is of the form remote:path
   163  //
   164  // Remotes are looked up in the config file.  If the remote isn't
   165  // found then NotFoundInConfigFile will be returned.
   166  //
   167  // On Windows avoid single character remote names as they can be mixed
   168  // up with drive letters.
   169  func NewFs(name string, root string, config configmap.Mapper) (fs.Fs, error) {
   170  	opt := new(Options)
   171  	err := configstruct.Set(config, opt)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	// If using a Shared Folder override root
   177  	if opt.SharedFolder != "" {
   178  		root = ""
   179  	}
   180  
   181  	//workaround for wonky parser
   182  	root = strings.Trim(root, "/")
   183  
   184  	f := &Fs{
   185  		name:       name,
   186  		root:       root,
   187  		opt:        *opt,
   188  		pacer:      fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
   189  		baseClient: &http.Client{},
   190  	}
   191  
   192  	f.features = (&fs.Features{
   193  		DuplicateFiles:          true,
   194  		CanHaveEmptyDirectories: true,
   195  	}).Fill(f)
   196  
   197  	client := fshttp.NewClient(fs.Config)
   198  
   199  	f.rest = rest.NewClient(client).SetRoot(apiBaseURL)
   200  
   201  	f.rest.SetHeader("Authorization", "Bearer "+f.opt.APIKey)
   202  
   203  	f.dirCache = dircache.New(root, rootID, f)
   204  
   205  	ctx := context.Background()
   206  
   207  	// Find the current root
   208  	err = f.dirCache.FindRoot(ctx, false)
   209  	if err != nil {
   210  		// Assume it is a file
   211  		newRoot, remote := dircache.SplitPath(root)
   212  		tempF := *f
   213  		tempF.dirCache = dircache.New(newRoot, rootID, &tempF)
   214  		tempF.root = newRoot
   215  		// Make new Fs which is the parent
   216  		err = tempF.dirCache.FindRoot(ctx, false)
   217  		if err != nil {
   218  			// No root so return old f
   219  			return f, nil
   220  		}
   221  		_, err := tempF.NewObject(ctx, remote)
   222  		if err != nil {
   223  			if err == fs.ErrorObjectNotFound {
   224  				// File doesn't exist so return old f
   225  				return f, nil
   226  			}
   227  			return nil, err
   228  		}
   229  		f.features.Fill(&tempF)
   230  		// XXX: update the old f here instead of returning tempF, since
   231  		// `features` were already filled with functions having *f as a receiver.
   232  		// See https://github.com/rclone/rclone/issues/2182
   233  		f.dirCache = tempF.dirCache
   234  		f.root = tempF.root
   235  		// return an error with an fs which points to the parent
   236  		return f, fs.ErrorIsFile
   237  	}
   238  	return f, nil
   239  }
   240  
   241  // List the objects and directories in dir into entries.  The
   242  // entries can be returned in any order but should be for a
   243  // complete directory.
   244  //
   245  // dir should be "" to list the root, and should not have
   246  // trailing slashes.
   247  //
   248  // This should return ErrDirNotFound if the directory isn't
   249  // found.
   250  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   251  	if f.opt.SharedFolder != "" {
   252  		return f.listSharedFiles(ctx, f.opt.SharedFolder)
   253  	}
   254  
   255  	dirContent, err := f.listDir(ctx, dir)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	return dirContent, nil
   261  }
   262  
   263  // NewObject finds the Object at remote.  If it can't be found
   264  // it returns the error ErrorObjectNotFound.
   265  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   266  	leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, false)
   267  	if err != nil {
   268  		if err == fs.ErrorDirNotFound {
   269  			return nil, fs.ErrorObjectNotFound
   270  		}
   271  		return nil, err
   272  	}
   273  
   274  	folderID, err := strconv.Atoi(directoryID)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	files, err := f.listFiles(ctx, folderID)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	for _, file := range files.Items {
   284  		if file.Filename == leaf {
   285  			path, ok := f.dirCache.GetInv(directoryID)
   286  
   287  			if !ok {
   288  				return nil, errors.New("Cannot find dir in dircache")
   289  			}
   290  
   291  			return f.newObjectFromFile(ctx, path, file), nil
   292  		}
   293  	}
   294  
   295  	return nil, fs.ErrorObjectNotFound
   296  }
   297  
   298  // Put in to the remote path with the modTime given of the given size
   299  //
   300  // When called from outside a Fs by rclone, src.Size() will always be >= 0.
   301  // But for unknown-sized objects (indicated by src.Size() == -1), Put should either
   302  // return an error or upload it properly (rather than e.g. calling panic).
   303  //
   304  // May create the object even if it returns an error - if so
   305  // will return the object and the error, otherwise will return
   306  // nil and the error
   307  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   308  	exisitingObj, err := f.NewObject(ctx, src.Remote())
   309  	switch err {
   310  	case nil:
   311  		return exisitingObj, exisitingObj.Update(ctx, in, src, options...)
   312  	case fs.ErrorObjectNotFound:
   313  		// Not found so create it
   314  		return f.PutUnchecked(ctx, in, src, options...)
   315  	default:
   316  		return nil, err
   317  	}
   318  }
   319  
   320  // putUnchecked uploads the object with the given name and size
   321  //
   322  // This will create a duplicate if we upload a new file without
   323  // checking to see if there is one already - use Put() for that.
   324  func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size int64, options ...fs.OpenOption) (fs.Object, error) {
   325  	if size > int64(100e9) {
   326  		return nil, errors.New("File too big, cant upload")
   327  	} else if size == 0 {
   328  		return nil, fs.ErrorCantUploadEmptyFiles
   329  	}
   330  
   331  	nodeResponse, err := f.getUploadNode(ctx)
   332  	if err != nil {
   333  		return nil, err
   334  	}
   335  
   336  	leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, true)
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  
   341  	_, err = f.uploadFile(ctx, in, size, leaf, directoryID, nodeResponse.ID, nodeResponse.URL)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  
   346  	fileUploadResponse, err := f.endUpload(ctx, nodeResponse.ID, nodeResponse.URL)
   347  	if err != nil {
   348  		return nil, err
   349  	}
   350  
   351  	if len(fileUploadResponse.Links) != 1 {
   352  		return nil, errors.New("unexpected amount of files")
   353  	}
   354  
   355  	link := fileUploadResponse.Links[0]
   356  	fileSize, err := strconv.ParseInt(link.Size, 10, 64)
   357  
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  
   362  	return &Object{
   363  		fs:     f,
   364  		remote: remote,
   365  		file: File{
   366  			ACL:         0,
   367  			CDN:         0,
   368  			Checksum:    link.Whirlpool,
   369  			ContentType: "",
   370  			Date:        time.Now().Format("2006-01-02 15:04:05"),
   371  			Filename:    link.Filename,
   372  			Pass:        0,
   373  			Size:        fileSize,
   374  			URL:         link.Download,
   375  		},
   376  	}, nil
   377  }
   378  
   379  // PutUnchecked uploads the object
   380  //
   381  // This will create a duplicate if we upload a new file without
   382  // checking to see if there is one already - use Put() for that.
   383  func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   384  	return f.putUnchecked(ctx, in, src.Remote(), src.Size(), options...)
   385  }
   386  
   387  // Mkdir makes the directory (container, bucket)
   388  //
   389  // Shouldn't return an error if it already exists
   390  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   391  	err := f.dirCache.FindRoot(ctx, true)
   392  	if err != nil {
   393  		return err
   394  	}
   395  	if dir != "" {
   396  		_, err = f.dirCache.FindDir(ctx, dir, true)
   397  	}
   398  	return err
   399  }
   400  
   401  // Rmdir removes the directory (container, bucket) if empty
   402  //
   403  // Return an error if it doesn't exist or isn't empty
   404  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   405  	err := f.dirCache.FindRoot(ctx, false)
   406  	if err != nil {
   407  		return err
   408  	}
   409  
   410  	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
   411  	if err != nil {
   412  		return err
   413  	}
   414  
   415  	folderID, err := strconv.Atoi(directoryID)
   416  	if err != nil {
   417  		return err
   418  	}
   419  
   420  	_, err = f.removeFolder(ctx, dir, folderID)
   421  	if err != nil {
   422  		return err
   423  	}
   424  
   425  	f.dirCache.FlushDir(dir)
   426  
   427  	return nil
   428  }
   429  
   430  // Check the interfaces are satisfied
   431  var (
   432  	_ fs.Fs              = (*Fs)(nil)
   433  	_ fs.PutUncheckeder  = (*Fs)(nil)
   434  	_ dircache.DirCacher = (*Fs)(nil)
   435  )