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

     1  package koofr
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"path"
    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/config/obscure"
    19  	"github.com/rclone/rclone/fs/fshttp"
    20  	"github.com/rclone/rclone/fs/hash"
    21  	"github.com/rclone/rclone/lib/encoder"
    22  
    23  	httpclient "github.com/koofr/go-httpclient"
    24  	koofrclient "github.com/koofr/go-koofrclient"
    25  )
    26  
    27  // Register Fs with rclone
    28  func init() {
    29  	fs.Register(&fs.RegInfo{
    30  		Name:        "koofr",
    31  		Description: "Koofr",
    32  		NewFs:       NewFs,
    33  		Options: []fs.Option{{
    34  			Name:     "endpoint",
    35  			Help:     "The Koofr API endpoint to use",
    36  			Default:  "https://app.koofr.net",
    37  			Required: true,
    38  			Advanced: true,
    39  		}, {
    40  			Name:     "mountid",
    41  			Help:     "Mount ID of the mount to use. If omitted, the primary mount is used.",
    42  			Required: false,
    43  			Default:  "",
    44  			Advanced: true,
    45  		}, {
    46  			Name:     "setmtime",
    47  			Help:     "Does the backend support setting modification time. Set this to false if you use a mount ID that points to a Dropbox or Amazon Drive backend.",
    48  			Default:  true,
    49  			Required: true,
    50  			Advanced: true,
    51  		}, {
    52  			Name:     "user",
    53  			Help:     "Your Koofr user name",
    54  			Required: true,
    55  		}, {
    56  			Name:       "password",
    57  			Help:       "Your Koofr password for rclone (generate one at https://app.koofr.net/app/admin/preferences/password)",
    58  			IsPassword: true,
    59  			Required:   true,
    60  		}, {
    61  			Name:     config.ConfigEncoding,
    62  			Help:     config.ConfigEncodingHelp,
    63  			Advanced: true,
    64  			// Encode invalid UTF-8 bytes as json doesn't handle them properly.
    65  			Default: (encoder.Display |
    66  				encoder.EncodeBackSlash |
    67  				encoder.EncodeInvalidUtf8),
    68  		}},
    69  	})
    70  }
    71  
    72  // Options represent the configuration of the Koofr backend
    73  type Options struct {
    74  	Endpoint string               `config:"endpoint"`
    75  	MountID  string               `config:"mountid"`
    76  	User     string               `config:"user"`
    77  	Password string               `config:"password"`
    78  	SetMTime bool                 `config:"setmtime"`
    79  	Enc      encoder.MultiEncoder `config:"encoding"`
    80  }
    81  
    82  // An Fs is a representation of a remote Koofr Fs
    83  type Fs struct {
    84  	name     string
    85  	mountID  string
    86  	root     string
    87  	opt      Options
    88  	features *fs.Features
    89  	client   *koofrclient.KoofrClient
    90  }
    91  
    92  // An Object on the remote Koofr Fs
    93  type Object struct {
    94  	fs     *Fs
    95  	remote string
    96  	info   koofrclient.FileInfo
    97  }
    98  
    99  func base(pth string) string {
   100  	rv := path.Base(pth)
   101  	if rv == "" || rv == "." {
   102  		rv = "/"
   103  	}
   104  	return rv
   105  }
   106  
   107  func dir(pth string) string {
   108  	rv := path.Dir(pth)
   109  	if rv == "" || rv == "." {
   110  		rv = "/"
   111  	}
   112  	return rv
   113  }
   114  
   115  // String returns a string representation of the remote Object
   116  func (o *Object) String() string {
   117  	return o.remote
   118  }
   119  
   120  // Remote returns the remote path of the Object, relative to Fs root
   121  func (o *Object) Remote() string {
   122  	return o.remote
   123  }
   124  
   125  // ModTime returns the modification time of the Object
   126  func (o *Object) ModTime(ctx context.Context) time.Time {
   127  	return time.Unix(o.info.Modified/1000, (o.info.Modified%1000)*1000*1000)
   128  }
   129  
   130  // Size return the size of the Object in bytes
   131  func (o *Object) Size() int64 {
   132  	return o.info.Size
   133  }
   134  
   135  // Fs returns a reference to the Koofr Fs containing the Object
   136  func (o *Object) Fs() fs.Info {
   137  	return o.fs
   138  }
   139  
   140  // Hash returns an MD5 hash of the Object
   141  func (o *Object) Hash(ctx context.Context, typ hash.Type) (string, error) {
   142  	if typ == hash.MD5 {
   143  		return o.info.Hash, nil
   144  	}
   145  	return "", nil
   146  }
   147  
   148  // fullPath returns full path of the remote Object (including Fs root)
   149  func (o *Object) fullPath() string {
   150  	return o.fs.fullPath(o.remote)
   151  }
   152  
   153  // Storable returns true if the Object is storable
   154  func (o *Object) Storable() bool {
   155  	return true
   156  }
   157  
   158  // SetModTime is not supported
   159  func (o *Object) SetModTime(ctx context.Context, mtime time.Time) error {
   160  	return fs.ErrorCantSetModTimeWithoutDelete
   161  }
   162  
   163  // Open opens the Object for reading
   164  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
   165  	var sOff, eOff int64 = 0, -1
   166  
   167  	fs.FixRangeOption(options, o.Size())
   168  	for _, option := range options {
   169  		switch x := option.(type) {
   170  		case *fs.SeekOption:
   171  			sOff = x.Offset
   172  		case *fs.RangeOption:
   173  			sOff = x.Start
   174  			eOff = x.End
   175  		default:
   176  			if option.Mandatory() {
   177  				fs.Logf(o, "Unsupported mandatory option: %v", option)
   178  			}
   179  		}
   180  	}
   181  	if sOff == 0 && eOff < 0 {
   182  		return o.fs.client.FilesGet(o.fs.mountID, o.fullPath())
   183  	}
   184  	span := &koofrclient.FileSpan{
   185  		Start: sOff,
   186  		End:   eOff,
   187  	}
   188  	return o.fs.client.FilesGetRange(o.fs.mountID, o.fullPath(), span)
   189  }
   190  
   191  // Update updates the Object contents
   192  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
   193  	mtime := src.ModTime(ctx).UnixNano() / 1000 / 1000
   194  	putopts := &koofrclient.PutOptions{
   195  		ForceOverwrite:             true,
   196  		NoRename:                   true,
   197  		OverwriteIgnoreNonExisting: true,
   198  		SetModified:                &mtime,
   199  	}
   200  	fullPath := o.fullPath()
   201  	dirPath := dir(fullPath)
   202  	name := base(fullPath)
   203  	err := o.fs.mkdir(dirPath)
   204  	if err != nil {
   205  		return err
   206  	}
   207  	info, err := o.fs.client.FilesPutWithOptions(o.fs.mountID, dirPath, name, in, putopts)
   208  	if err != nil {
   209  		return err
   210  	}
   211  	o.info = *info
   212  	return nil
   213  }
   214  
   215  // Remove deletes the remote Object
   216  func (o *Object) Remove(ctx context.Context) error {
   217  	return o.fs.client.FilesDelete(o.fs.mountID, o.fullPath())
   218  }
   219  
   220  // Name returns the name of the Fs
   221  func (f *Fs) Name() string {
   222  	return f.name
   223  }
   224  
   225  // Root returns the root path of the Fs
   226  func (f *Fs) Root() string {
   227  	return f.root
   228  }
   229  
   230  // String returns a string representation of the Fs
   231  func (f *Fs) String() string {
   232  	return "koofr:" + f.mountID + ":" + f.root
   233  }
   234  
   235  // Features returns the optional features supported by this Fs
   236  func (f *Fs) Features() *fs.Features {
   237  	return f.features
   238  }
   239  
   240  // Precision denotes that setting modification times is not supported
   241  func (f *Fs) Precision() time.Duration {
   242  	if !f.opt.SetMTime {
   243  		return fs.ModTimeNotSupported
   244  	}
   245  	return time.Millisecond
   246  }
   247  
   248  // Hashes returns a set of hashes are Provided by the Fs
   249  func (f *Fs) Hashes() hash.Set {
   250  	return hash.Set(hash.MD5)
   251  }
   252  
   253  // fullPath constructs a full, absolute path from an Fs root relative path,
   254  func (f *Fs) fullPath(part string) string {
   255  	return f.opt.Enc.FromStandardPath(path.Join("/", f.root, part))
   256  }
   257  
   258  // NewFs constructs a new filesystem given a root path and configuration options
   259  func NewFs(name, root string, m configmap.Mapper) (ff fs.Fs, err error) {
   260  	opt := new(Options)
   261  	err = configstruct.Set(m, opt)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	pass, err := obscure.Reveal(opt.Password)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	httpClient := httpclient.New()
   270  	httpClient.Client = fshttp.NewClient(fs.Config)
   271  	client := koofrclient.NewKoofrClientWithHTTPClient(opt.Endpoint, httpClient)
   272  	basicAuth := fmt.Sprintf("Basic %s",
   273  		base64.StdEncoding.EncodeToString([]byte(opt.User+":"+pass)))
   274  	client.HTTPClient.Headers.Set("Authorization", basicAuth)
   275  	mounts, err := client.Mounts()
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	f := &Fs{
   280  		name:   name,
   281  		root:   root,
   282  		opt:    *opt,
   283  		client: client,
   284  	}
   285  	f.features = (&fs.Features{
   286  		CaseInsensitive:         true,
   287  		DuplicateFiles:          false,
   288  		BucketBased:             false,
   289  		CanHaveEmptyDirectories: true,
   290  	}).Fill(f)
   291  	for _, m := range mounts {
   292  		if opt.MountID != "" {
   293  			if m.Id == opt.MountID {
   294  				f.mountID = m.Id
   295  				break
   296  			}
   297  		} else if m.IsPrimary {
   298  			f.mountID = m.Id
   299  			break
   300  		}
   301  	}
   302  	if f.mountID == "" {
   303  		if opt.MountID == "" {
   304  			return nil, errors.New("Failed to find primary mount")
   305  		}
   306  		return nil, errors.New("Failed to find mount " + opt.MountID)
   307  	}
   308  	rootFile, err := f.client.FilesInfo(f.mountID, f.opt.Enc.FromStandardPath("/"+f.root))
   309  	if err == nil && rootFile.Type != "dir" {
   310  		f.root = dir(f.root)
   311  		err = fs.ErrorIsFile
   312  	} else {
   313  		err = nil
   314  	}
   315  	return f, err
   316  }
   317  
   318  // List returns a list of items in a directory
   319  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   320  	files, err := f.client.FilesList(f.mountID, f.fullPath(dir))
   321  	if err != nil {
   322  		return nil, translateErrorsDir(err)
   323  	}
   324  	entries = make([]fs.DirEntry, len(files))
   325  	for i, file := range files {
   326  		remote := path.Join(dir, f.opt.Enc.ToStandardName(file.Name))
   327  		if file.Type == "dir" {
   328  			entries[i] = fs.NewDir(remote, time.Unix(0, 0))
   329  		} else {
   330  			entries[i] = &Object{
   331  				fs:     f,
   332  				info:   file,
   333  				remote: remote,
   334  			}
   335  		}
   336  	}
   337  	return entries, nil
   338  }
   339  
   340  // NewObject creates a new remote Object for a given remote path
   341  func (f *Fs) NewObject(ctx context.Context, remote string) (obj fs.Object, err error) {
   342  	info, err := f.client.FilesInfo(f.mountID, f.fullPath(remote))
   343  	if err != nil {
   344  		return nil, translateErrorsObject(err)
   345  	}
   346  	if info.Type == "dir" {
   347  		return nil, fs.ErrorNotAFile
   348  	}
   349  	return &Object{
   350  		fs:     f,
   351  		info:   info,
   352  		remote: remote,
   353  	}, nil
   354  }
   355  
   356  // Put updates a remote Object
   357  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (obj fs.Object, err error) {
   358  	mtime := src.ModTime(ctx).UnixNano() / 1000 / 1000
   359  	putopts := &koofrclient.PutOptions{
   360  		ForceOverwrite:             true,
   361  		NoRename:                   true,
   362  		OverwriteIgnoreNonExisting: true,
   363  		SetModified:                &mtime,
   364  	}
   365  	fullPath := f.fullPath(src.Remote())
   366  	dirPath := dir(fullPath)
   367  	name := base(fullPath)
   368  	err = f.mkdir(dirPath)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  	info, err := f.client.FilesPutWithOptions(f.mountID, dirPath, name, in, putopts)
   373  	if err != nil {
   374  		return nil, translateErrorsObject(err)
   375  	}
   376  	return &Object{
   377  		fs:     f,
   378  		info:   *info,
   379  		remote: src.Remote(),
   380  	}, nil
   381  }
   382  
   383  // PutStream updates a remote Object with a stream of unknown size
   384  func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   385  	return f.Put(ctx, in, src, options...)
   386  }
   387  
   388  // isBadRequest is a predicate which holds true iff the error returned was
   389  // HTTP status 400
   390  func isBadRequest(err error) bool {
   391  	switch err := err.(type) {
   392  	case httpclient.InvalidStatusError:
   393  		if err.Got == http.StatusBadRequest {
   394  			return true
   395  		}
   396  	}
   397  	return false
   398  }
   399  
   400  // translateErrorsDir translates koofr errors to rclone errors (for a dir
   401  // operation)
   402  func translateErrorsDir(err error) error {
   403  	switch err := err.(type) {
   404  	case httpclient.InvalidStatusError:
   405  		if err.Got == http.StatusNotFound {
   406  			return fs.ErrorDirNotFound
   407  		}
   408  	}
   409  	return err
   410  }
   411  
   412  // translatesErrorsObject translates Koofr errors to rclone errors (for an object operation)
   413  func translateErrorsObject(err error) error {
   414  	switch err := err.(type) {
   415  	case httpclient.InvalidStatusError:
   416  		if err.Got == http.StatusNotFound {
   417  			return fs.ErrorObjectNotFound
   418  		}
   419  	}
   420  	return err
   421  }
   422  
   423  // mkdir creates a directory at the given remote path. Creates ancestors if
   424  // necessary
   425  func (f *Fs) mkdir(fullPath string) error {
   426  	if fullPath == "/" {
   427  		return nil
   428  	}
   429  	info, err := f.client.FilesInfo(f.mountID, fullPath)
   430  	if err == nil && info.Type == "dir" {
   431  		return nil
   432  	}
   433  	err = translateErrorsDir(err)
   434  	if err != nil && err != fs.ErrorDirNotFound {
   435  		return err
   436  	}
   437  	dirs := strings.Split(fullPath, "/")
   438  	parent := "/"
   439  	for _, part := range dirs {
   440  		if part == "" {
   441  			continue
   442  		}
   443  		info, err = f.client.FilesInfo(f.mountID, path.Join(parent, part))
   444  		if err != nil || info.Type != "dir" {
   445  			err = translateErrorsDir(err)
   446  			if err != nil && err != fs.ErrorDirNotFound {
   447  				return err
   448  			}
   449  			err = f.client.FilesNewFolder(f.mountID, parent, part)
   450  			if err != nil && !isBadRequest(err) {
   451  				return err
   452  			}
   453  		}
   454  		parent = path.Join(parent, part)
   455  	}
   456  	return nil
   457  }
   458  
   459  // Mkdir creates a directory at the given remote path. Creates ancestors if
   460  // necessary
   461  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   462  	fullPath := f.fullPath(dir)
   463  	return f.mkdir(fullPath)
   464  }
   465  
   466  // Rmdir removes an (empty) directory at the given remote path
   467  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   468  	files, err := f.client.FilesList(f.mountID, f.fullPath(dir))
   469  	if err != nil {
   470  		return translateErrorsDir(err)
   471  	}
   472  	if len(files) > 0 {
   473  		return fs.ErrorDirectoryNotEmpty
   474  	}
   475  	err = f.client.FilesDelete(f.mountID, f.fullPath(dir))
   476  	if err != nil {
   477  		return translateErrorsDir(err)
   478  	}
   479  	return nil
   480  }
   481  
   482  // Copy copies a remote Object to the given path
   483  func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   484  	dstFullPath := f.fullPath(remote)
   485  	dstDir := dir(dstFullPath)
   486  	err := f.mkdir(dstDir)
   487  	if err != nil {
   488  		return nil, fs.ErrorCantCopy
   489  	}
   490  	mtime := src.ModTime(ctx).UnixNano() / 1000 / 1000
   491  	err = f.client.FilesCopy((src.(*Object)).fs.mountID,
   492  		(src.(*Object)).fs.fullPath((src.(*Object)).remote),
   493  		f.mountID, dstFullPath, koofrclient.CopyOptions{SetModified: &mtime})
   494  	if err != nil {
   495  		return nil, fs.ErrorCantCopy
   496  	}
   497  	return f.NewObject(ctx, remote)
   498  }
   499  
   500  // Move moves a remote Object to the given path
   501  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   502  	srcObj := src.(*Object)
   503  	dstFullPath := f.fullPath(remote)
   504  	dstDir := dir(dstFullPath)
   505  	err := f.mkdir(dstDir)
   506  	if err != nil {
   507  		return nil, fs.ErrorCantMove
   508  	}
   509  	err = f.client.FilesMove(srcObj.fs.mountID,
   510  		srcObj.fs.fullPath(srcObj.remote), f.mountID, dstFullPath)
   511  	if err != nil {
   512  		return nil, fs.ErrorCantMove
   513  	}
   514  	return f.NewObject(ctx, remote)
   515  }
   516  
   517  // DirMove moves a remote directory to the given path
   518  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
   519  	srcFs := src.(*Fs)
   520  	srcFullPath := srcFs.fullPath(srcRemote)
   521  	dstFullPath := f.fullPath(dstRemote)
   522  	if srcFs.mountID == f.mountID && srcFullPath == dstFullPath {
   523  		return fs.ErrorDirExists
   524  	}
   525  	dstDir := dir(dstFullPath)
   526  	err := f.mkdir(dstDir)
   527  	if err != nil {
   528  		return fs.ErrorCantDirMove
   529  	}
   530  	err = f.client.FilesMove(srcFs.mountID, srcFullPath, f.mountID, dstFullPath)
   531  	if err != nil {
   532  		return fs.ErrorCantDirMove
   533  	}
   534  	return nil
   535  }
   536  
   537  // About reports space usage (with a MB precision)
   538  func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
   539  	mount, err := f.client.MountsDetails(f.mountID)
   540  	if err != nil {
   541  		return nil, err
   542  	}
   543  	return &fs.Usage{
   544  		Total:   fs.NewUsageValue(mount.SpaceTotal * 1024 * 1024),
   545  		Used:    fs.NewUsageValue(mount.SpaceUsed * 1024 * 1024),
   546  		Trashed: nil,
   547  		Other:   nil,
   548  		Free:    fs.NewUsageValue((mount.SpaceTotal - mount.SpaceUsed) * 1024 * 1024),
   549  		Objects: nil,
   550  	}, nil
   551  }
   552  
   553  // Purge purges the complete Fs
   554  func (f *Fs) Purge(ctx context.Context) error {
   555  	err := translateErrorsDir(f.client.FilesDelete(f.mountID, f.fullPath("")))
   556  	return err
   557  }
   558  
   559  // linkCreate is a Koofr API request for creating a public link
   560  type linkCreate struct {
   561  	Path string `json:"path"`
   562  }
   563  
   564  // link is a Koofr API response to creating a public link
   565  type link struct {
   566  	ID               string `json:"id"`
   567  	Name             string `json:"name"`
   568  	Path             string `json:"path"`
   569  	Counter          int64  `json:"counter"`
   570  	URL              string `json:"url"`
   571  	ShortURL         string `json:"shortUrl"`
   572  	Hash             string `json:"hash"`
   573  	Host             string `json:"host"`
   574  	HasPassword      bool   `json:"hasPassword"`
   575  	Password         string `json:"password"`
   576  	ValidFrom        int64  `json:"validFrom"`
   577  	ValidTo          int64  `json:"validTo"`
   578  	PasswordRequired bool   `json:"passwordRequired"`
   579  }
   580  
   581  // createLink makes a Koofr API call to create a public link
   582  func createLink(c *koofrclient.KoofrClient, mountID string, path string) (*link, error) {
   583  	linkCreate := linkCreate{
   584  		Path: path,
   585  	}
   586  	linkData := link{}
   587  
   588  	request := httpclient.RequestData{
   589  		Method:         "POST",
   590  		Path:           "/api/v2/mounts/" + mountID + "/links",
   591  		ExpectedStatus: []int{http.StatusOK, http.StatusCreated},
   592  		ReqEncoding:    httpclient.EncodingJSON,
   593  		ReqValue:       linkCreate,
   594  		RespEncoding:   httpclient.EncodingJSON,
   595  		RespValue:      &linkData,
   596  	}
   597  
   598  	_, err := c.Request(&request)
   599  	if err != nil {
   600  		return nil, err
   601  	}
   602  	return &linkData, nil
   603  }
   604  
   605  // PublicLink creates a public link to the remote path
   606  func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) {
   607  	linkData, err := createLink(f.client, f.mountID, f.fullPath(remote))
   608  	if err != nil {
   609  		return "", translateErrorsDir(err)
   610  	}
   611  	return linkData.ShortURL, nil
   612  }