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