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

     1  // Package imagekit provides an interface to the ImageKit.io media library.
     2  package imagekit
     3  
     4  import (
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"math"
    10  	"net/http"
    11  	"path"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/rclone/rclone/backend/imagekit/client"
    17  	"github.com/rclone/rclone/fs"
    18  	"github.com/rclone/rclone/fs/config"
    19  	"github.com/rclone/rclone/fs/config/configmap"
    20  	"github.com/rclone/rclone/fs/config/configstruct"
    21  	"github.com/rclone/rclone/fs/hash"
    22  	"github.com/rclone/rclone/lib/encoder"
    23  	"github.com/rclone/rclone/lib/pacer"
    24  	"github.com/rclone/rclone/lib/readers"
    25  	"github.com/rclone/rclone/lib/version"
    26  )
    27  
    28  const (
    29  	minSleep      = 1 * time.Millisecond
    30  	maxSleep      = 100 * time.Millisecond
    31  	decayConstant = 2
    32  )
    33  
    34  var systemMetadataInfo = map[string]fs.MetadataHelp{
    35  	"btime": {
    36  		Help:     "Time of file birth (creation) read from Last-Modified header",
    37  		Type:     "RFC 3339",
    38  		Example:  "2006-01-02T15:04:05.999999999Z07:00",
    39  		ReadOnly: true,
    40  	},
    41  	"size": {
    42  		Help:     "Size of the object in bytes",
    43  		Type:     "int64",
    44  		ReadOnly: true,
    45  	},
    46  	"file-type": {
    47  		Help:     "Type of the file",
    48  		Type:     "string",
    49  		Example:  "image",
    50  		ReadOnly: true,
    51  	},
    52  	"height": {
    53  		Help:     "Height of the image or video in pixels",
    54  		Type:     "int",
    55  		ReadOnly: true,
    56  	},
    57  	"width": {
    58  		Help:     "Width of the image or video in pixels",
    59  		Type:     "int",
    60  		ReadOnly: true,
    61  	},
    62  	"has-alpha": {
    63  		Help:     "Whether the image has alpha channel or not",
    64  		Type:     "bool",
    65  		ReadOnly: true,
    66  	},
    67  	"tags": {
    68  		Help:     "Tags associated with the file",
    69  		Type:     "string",
    70  		Example:  "tag1,tag2",
    71  		ReadOnly: true,
    72  	},
    73  	"google-tags": {
    74  		Help:     "AI generated tags by Google Cloud Vision associated with the image",
    75  		Type:     "string",
    76  		Example:  "tag1,tag2",
    77  		ReadOnly: true,
    78  	},
    79  	"aws-tags": {
    80  		Help:     "AI generated tags by AWS Rekognition associated with the image",
    81  		Type:     "string",
    82  		Example:  "tag1,tag2",
    83  		ReadOnly: true,
    84  	},
    85  	"is-private-file": {
    86  		Help:     "Whether the file is private or not",
    87  		Type:     "bool",
    88  		ReadOnly: true,
    89  	},
    90  	"custom-coordinates": {
    91  		Help:     "Custom coordinates of the file",
    92  		Type:     "string",
    93  		Example:  "0,0,100,100",
    94  		ReadOnly: true,
    95  	},
    96  }
    97  
    98  // Register with Fs
    99  func init() {
   100  	fs.Register(&fs.RegInfo{
   101  		Name:        "imagekit",
   102  		Description: "ImageKit.io",
   103  		NewFs:       NewFs,
   104  		MetadataInfo: &fs.MetadataInfo{
   105  			System: systemMetadataInfo,
   106  			Help:   `Any metadata supported by the underlying remote is read and written.`,
   107  		},
   108  		Options: []fs.Option{
   109  			{
   110  				Name:     "endpoint",
   111  				Help:     "You can find your ImageKit.io URL endpoint in your [dashboard](https://imagekit.io/dashboard/developer/api-keys)",
   112  				Required: true,
   113  			},
   114  			{
   115  				Name:      "public_key",
   116  				Help:      "You can find your ImageKit.io public key in your [dashboard](https://imagekit.io/dashboard/developer/api-keys)",
   117  				Required:  true,
   118  				Sensitive: true,
   119  			},
   120  			{
   121  				Name:      "private_key",
   122  				Help:      "You can find your ImageKit.io private key in your [dashboard](https://imagekit.io/dashboard/developer/api-keys)",
   123  				Required:  true,
   124  				Sensitive: true,
   125  			},
   126  			{
   127  				Name:     "only_signed",
   128  				Help:     "If you have configured `Restrict unsigned image URLs` in your dashboard settings, set this to true.",
   129  				Default:  false,
   130  				Advanced: true,
   131  			},
   132  			{
   133  				Name:     "versions",
   134  				Help:     "Include old versions in directory listings.",
   135  				Default:  false,
   136  				Advanced: true,
   137  			},
   138  			{
   139  				Name:     "upload_tags",
   140  				Help:     "Tags to add to the uploaded files, e.g. \"tag1,tag2\".",
   141  				Default:  "",
   142  				Advanced: true,
   143  			},
   144  			{
   145  				Name:     config.ConfigEncoding,
   146  				Help:     config.ConfigEncodingHelp,
   147  				Advanced: true,
   148  				Default: (encoder.EncodeZero |
   149  					encoder.EncodeSlash |
   150  					encoder.EncodeQuestion |
   151  					encoder.EncodeHashPercent |
   152  					encoder.EncodeCtl |
   153  					encoder.EncodeDel |
   154  					encoder.EncodeDot |
   155  					encoder.EncodeDoubleQuote |
   156  					encoder.EncodePercent |
   157  					encoder.EncodeBackSlash |
   158  					encoder.EncodeDollar |
   159  					encoder.EncodeLtGt |
   160  					encoder.EncodeSquareBracket |
   161  					encoder.EncodeInvalidUtf8),
   162  			},
   163  		},
   164  	})
   165  }
   166  
   167  // Options defines the configuration for this backend
   168  type Options struct {
   169  	Endpoint   string               `config:"endpoint"`
   170  	PublicKey  string               `config:"public_key"`
   171  	PrivateKey string               `config:"private_key"`
   172  	OnlySigned bool                 `config:"only_signed"`
   173  	Versions   bool                 `config:"versions"`
   174  	Enc        encoder.MultiEncoder `config:"encoding"`
   175  }
   176  
   177  // Fs represents a remote to ImageKit
   178  type Fs struct {
   179  	name     string           // name of remote
   180  	root     string           // root path
   181  	opt      Options          // parsed options
   182  	features *fs.Features     // optional features
   183  	ik       *client.ImageKit // ImageKit client
   184  	pacer    *fs.Pacer        // pacer for API calls
   185  }
   186  
   187  // Object describes a ImageKit file
   188  type Object struct {
   189  	fs          *Fs         // The Fs this object is part of
   190  	remote      string      // The remote path
   191  	filePath    string      // The path to the file
   192  	contentType string      // The content type of the object if known - may be ""
   193  	timestamp   time.Time   // The timestamp of the object if known - may be zero
   194  	file        client.File // The media file if known - may be nil
   195  	versionID   string      // If present this points to an object version
   196  }
   197  
   198  // NewFs constructs an Fs from the path, container:path
   199  func NewFs(ctx context.Context, name string, root string, m configmap.Mapper) (fs.Fs, error) {
   200  	opt := new(Options)
   201  	err := configstruct.Set(m, opt)
   202  
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	ik, err := client.New(ctx, client.NewParams{
   208  		URLEndpoint: opt.Endpoint,
   209  		PublicKey:   opt.PublicKey,
   210  		PrivateKey:  opt.PrivateKey,
   211  	})
   212  
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	f := &Fs{
   218  		name:  name,
   219  		opt:   *opt,
   220  		ik:    ik,
   221  		pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
   222  	}
   223  
   224  	f.root = path.Join("/", root)
   225  
   226  	f.features = (&fs.Features{
   227  		CaseInsensitive:         false,
   228  		DuplicateFiles:          false,
   229  		ReadMimeType:            true,
   230  		WriteMimeType:           false,
   231  		CanHaveEmptyDirectories: true,
   232  		BucketBased:             false,
   233  		ServerSideAcrossConfigs: false,
   234  		IsLocal:                 false,
   235  		SlowHash:                true,
   236  		ReadMetadata:            true,
   237  		WriteMetadata:           false,
   238  		UserMetadata:            false,
   239  		FilterAware:             true,
   240  		PartialUploads:          false,
   241  		NoMultiThreading:        false,
   242  	}).Fill(ctx, f)
   243  
   244  	if f.root != "/" {
   245  
   246  		r := f.root
   247  
   248  		folderPath := f.EncodePath(r[:strings.LastIndex(r, "/")+1])
   249  		fileName := f.EncodeFileName(r[strings.LastIndex(r, "/")+1:])
   250  
   251  		file := f.getFileByName(ctx, folderPath, fileName)
   252  
   253  		if file != nil {
   254  			newRoot := path.Dir(f.root)
   255  			f.root = newRoot
   256  			return f, fs.ErrorIsFile
   257  		}
   258  
   259  	}
   260  	return f, nil
   261  }
   262  
   263  // Name of the remote (as passed into NewFs)
   264  func (f *Fs) Name() string {
   265  	return f.name
   266  }
   267  
   268  // Root of the remote (as passed into NewFs)
   269  func (f *Fs) Root() string {
   270  	return strings.TrimLeft(f.root, "/")
   271  }
   272  
   273  // String returns a description of the FS
   274  func (f *Fs) String() string {
   275  	return fmt.Sprintf("FS imagekit: %s", f.root)
   276  }
   277  
   278  // Precision of the ModTimes in this Fs
   279  func (f *Fs) Precision() time.Duration {
   280  	return fs.ModTimeNotSupported
   281  }
   282  
   283  // Hashes returns the supported hash types of the filesystem.
   284  func (f *Fs) Hashes() hash.Set {
   285  	return hash.NewHashSet()
   286  }
   287  
   288  // Features returns the optional features of this Fs.
   289  func (f *Fs) Features() *fs.Features {
   290  	return f.features
   291  }
   292  
   293  // List the objects and directories in dir into entries.  The
   294  // entries can be returned in any order but should be for a
   295  // complete directory.
   296  //
   297  // dir should be "" to list the root, and should not have
   298  // trailing slashes.
   299  //
   300  // This should return ErrDirNotFound if the directory isn't
   301  // found.
   302  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   303  
   304  	remote := path.Join(f.root, dir)
   305  
   306  	remote = f.EncodePath(remote)
   307  
   308  	if remote != "/" {
   309  		parentFolderPath, folderName := path.Split(remote)
   310  		folderExists, err := f.getFolderByName(ctx, parentFolderPath, folderName)
   311  
   312  		if err != nil {
   313  			return make(fs.DirEntries, 0), err
   314  		}
   315  
   316  		if folderExists == nil {
   317  			return make(fs.DirEntries, 0), fs.ErrorDirNotFound
   318  		}
   319  	}
   320  
   321  	folders, folderError := f.getFolders(ctx, remote)
   322  
   323  	if folderError != nil {
   324  		return make(fs.DirEntries, 0), folderError
   325  	}
   326  
   327  	files, fileError := f.getFiles(ctx, remote, f.opt.Versions)
   328  
   329  	if fileError != nil {
   330  		return make(fs.DirEntries, 0), fileError
   331  	}
   332  
   333  	res := make([]fs.DirEntry, 0, len(folders)+len(files))
   334  
   335  	for _, folder := range folders {
   336  		folderPath := f.DecodePath(strings.TrimLeft(strings.Replace(folder.FolderPath, f.EncodePath(f.root), "", 1), "/"))
   337  		res = append(res, fs.NewDir(folderPath, folder.UpdatedAt))
   338  	}
   339  
   340  	for _, file := range files {
   341  		res = append(res, f.newObject(ctx, remote, file))
   342  	}
   343  
   344  	return res, nil
   345  }
   346  
   347  func (f *Fs) newObject(ctx context.Context, remote string, file client.File) *Object {
   348  	remoteFile := strings.TrimLeft(strings.Replace(file.FilePath, f.EncodePath(f.root), "", 1), "/")
   349  
   350  	folderPath, fileName := path.Split(remoteFile)
   351  
   352  	folderPath = f.DecodePath(folderPath)
   353  	fileName = f.DecodeFileName(fileName)
   354  
   355  	remoteFile = path.Join(folderPath, fileName)
   356  
   357  	if file.Type == "file-version" {
   358  		remoteFile = version.Add(remoteFile, file.UpdatedAt)
   359  
   360  		return &Object{
   361  			fs:          f,
   362  			remote:      remoteFile,
   363  			filePath:    file.FilePath,
   364  			contentType: file.Mime,
   365  			timestamp:   file.UpdatedAt,
   366  			file:        file,
   367  			versionID:   file.VersionInfo["id"],
   368  		}
   369  	}
   370  
   371  	return &Object{
   372  		fs:          f,
   373  		remote:      remoteFile,
   374  		filePath:    file.FilePath,
   375  		contentType: file.Mime,
   376  		timestamp:   file.UpdatedAt,
   377  		file:        file,
   378  	}
   379  }
   380  
   381  // NewObject finds the Object at remote.  If it can't be found
   382  // it returns the error ErrorObjectNotFound.
   383  //
   384  // If remote points to a directory then it should return
   385  // ErrorIsDir if possible without doing any extra work,
   386  // otherwise ErrorObjectNotFound.
   387  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   388  	r := path.Join(f.root, remote)
   389  
   390  	folderPath, fileName := path.Split(r)
   391  
   392  	folderPath = f.EncodePath(folderPath)
   393  	fileName = f.EncodeFileName(fileName)
   394  
   395  	isFolder, err := f.getFolderByName(ctx, folderPath, fileName)
   396  
   397  	if err != nil {
   398  		return nil, err
   399  	}
   400  
   401  	if isFolder != nil {
   402  		return nil, fs.ErrorIsDir
   403  	}
   404  
   405  	file := f.getFileByName(ctx, folderPath, fileName)
   406  
   407  	if file == nil {
   408  		return nil, fs.ErrorObjectNotFound
   409  	}
   410  
   411  	return f.newObject(ctx, r, *file), nil
   412  }
   413  
   414  // Put in to the remote path with the modTime given of the given size
   415  //
   416  // When called from outside an Fs by rclone, src.Size() will always be >= 0.
   417  // But for unknown-sized objects (indicated by src.Size() == -1), Put should either
   418  // return an error or upload it properly (rather than e.g. calling panic).
   419  //
   420  // May create the object even if it returns an error - if so
   421  // will return the object and the error, otherwise will return
   422  // nil and the error
   423  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   424  	return uploadFile(ctx, f, in, src.Remote(), options...)
   425  }
   426  
   427  // Mkdir makes the directory (container, bucket)
   428  //
   429  // Shouldn't return an error if it already exists
   430  func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
   431  	remote := path.Join(f.root, dir)
   432  	parentFolderPath, folderName := path.Split(remote)
   433  
   434  	parentFolderPath = f.EncodePath(parentFolderPath)
   435  	folderName = f.EncodeFileName(folderName)
   436  
   437  	err = f.pacer.Call(func() (bool, error) {
   438  		var res *http.Response
   439  		res, err = f.ik.CreateFolder(ctx, client.CreateFolderParam{
   440  			ParentFolderPath: parentFolderPath,
   441  			FolderName:       folderName,
   442  		})
   443  
   444  		return f.shouldRetry(ctx, res, err)
   445  	})
   446  
   447  	return err
   448  }
   449  
   450  // Rmdir removes the directory (container, bucket) if empty
   451  //
   452  // Return an error if it doesn't exist or isn't empty
   453  func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) {
   454  
   455  	entries, err := f.List(ctx, dir)
   456  
   457  	if err != nil {
   458  		return err
   459  	}
   460  
   461  	if len(entries) > 0 {
   462  		return errors.New("directory is not empty")
   463  	}
   464  
   465  	err = f.pacer.Call(func() (bool, error) {
   466  		var res *http.Response
   467  		res, err = f.ik.DeleteFolder(ctx, client.DeleteFolderParam{
   468  			FolderPath: f.EncodePath(path.Join(f.root, dir)),
   469  		})
   470  
   471  		if res.StatusCode == http.StatusNotFound {
   472  			return false, fs.ErrorDirNotFound
   473  		}
   474  
   475  		return f.shouldRetry(ctx, res, err)
   476  	})
   477  
   478  	return err
   479  }
   480  
   481  // Purge deletes all the files and the container
   482  //
   483  // Optional interface: Only implement this if you have a way of
   484  // deleting all the files quicker than just running Remove() on the
   485  // result of List()
   486  func (f *Fs) Purge(ctx context.Context, dir string) (err error) {
   487  
   488  	remote := path.Join(f.root, dir)
   489  
   490  	err = f.pacer.Call(func() (bool, error) {
   491  		var res *http.Response
   492  		res, err = f.ik.DeleteFolder(ctx, client.DeleteFolderParam{
   493  			FolderPath: f.EncodePath(remote),
   494  		})
   495  
   496  		if res.StatusCode == http.StatusNotFound {
   497  			return false, fs.ErrorDirNotFound
   498  		}
   499  
   500  		return f.shouldRetry(ctx, res, err)
   501  	})
   502  
   503  	return err
   504  }
   505  
   506  // PublicLink generates a public link to the remote path (usually readable by anyone)
   507  func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
   508  
   509  	duration := time.Duration(math.Abs(float64(expire)))
   510  
   511  	expireSeconds := duration.Seconds()
   512  
   513  	fileRemote := path.Join(f.root, remote)
   514  
   515  	folderPath, fileName := path.Split(fileRemote)
   516  	folderPath = f.EncodePath(folderPath)
   517  	fileName = f.EncodeFileName(fileName)
   518  
   519  	file := f.getFileByName(ctx, folderPath, fileName)
   520  
   521  	if file == nil {
   522  		return "", fs.ErrorObjectNotFound
   523  	}
   524  
   525  	// Pacer not needed as this doesn't use the API
   526  	url, err := f.ik.URL(client.URLParam{
   527  		Src:           file.URL,
   528  		Signed:        *file.IsPrivateFile || f.opt.OnlySigned,
   529  		ExpireSeconds: int64(expireSeconds),
   530  		QueryParameters: map[string]string{
   531  			"updatedAt": file.UpdatedAt.String(),
   532  		},
   533  	})
   534  
   535  	if err != nil {
   536  		return "", err
   537  	}
   538  
   539  	return url, nil
   540  }
   541  
   542  // Fs returns read only access to the Fs that this object is part of
   543  func (o *Object) Fs() fs.Info {
   544  	return o.fs
   545  }
   546  
   547  // Hash returns the selected checksum of the file
   548  // If no checksum is available it returns ""
   549  func (o *Object) Hash(ctx context.Context, ty hash.Type) (string, error) {
   550  	return "", hash.ErrUnsupported
   551  }
   552  
   553  // Storable says whether this object can be stored
   554  func (o *Object) Storable() bool {
   555  	return true
   556  }
   557  
   558  // String returns a description of the Object
   559  func (o *Object) String() string {
   560  	if o == nil {
   561  		return "<nil>"
   562  	}
   563  	return o.file.Name
   564  }
   565  
   566  // Remote returns the remote path
   567  func (o *Object) Remote() string {
   568  	return o.remote
   569  }
   570  
   571  // ModTime returns the modification date of the file
   572  // It should return a best guess if one isn't available
   573  func (o *Object) ModTime(context.Context) time.Time {
   574  	return o.file.UpdatedAt
   575  }
   576  
   577  // Size returns the size of the file
   578  func (o *Object) Size() int64 {
   579  	return int64(o.file.Size)
   580  }
   581  
   582  // MimeType returns the MIME type of the file
   583  func (o *Object) MimeType(context.Context) string {
   584  	return o.contentType
   585  }
   586  
   587  // Open opens the file for read.  Call Close() on the returned io.ReadCloser
   588  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
   589  	// Offset and Count for range download
   590  	var offset int64
   591  	var count int64
   592  
   593  	fs.FixRangeOption(options, -1)
   594  	partialContent := false
   595  	for _, option := range options {
   596  		switch x := option.(type) {
   597  		case *fs.RangeOption:
   598  			offset, count = x.Decode(-1)
   599  			partialContent = true
   600  		case *fs.SeekOption:
   601  			offset = x.Offset
   602  			partialContent = true
   603  		default:
   604  			if option.Mandatory() {
   605  				fs.Logf(o, "Unsupported mandatory option: %v", option)
   606  			}
   607  		}
   608  	}
   609  
   610  	// Pacer not needed as this doesn't use the API
   611  	url, err := o.fs.ik.URL(client.URLParam{
   612  		Src:    o.file.URL,
   613  		Signed: *o.file.IsPrivateFile || o.fs.opt.OnlySigned,
   614  		QueryParameters: map[string]string{
   615  			"tr":        "orig-true",
   616  			"updatedAt": o.file.UpdatedAt.String(),
   617  		},
   618  	})
   619  
   620  	if err != nil {
   621  		return nil, err
   622  	}
   623  
   624  	client := &http.Client{}
   625  	req, _ := http.NewRequest("GET", url, nil)
   626  	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+count-1))
   627  	resp, err := client.Do(req)
   628  
   629  	if err != nil {
   630  		return nil, err
   631  	}
   632  
   633  	end := resp.ContentLength
   634  
   635  	if partialContent && resp.StatusCode == http.StatusOK {
   636  		skip := offset
   637  
   638  		if offset < 0 {
   639  			skip = end + offset + 1
   640  		}
   641  
   642  		_, err = io.CopyN(io.Discard, resp.Body, skip)
   643  		if err != nil {
   644  			if resp != nil {
   645  				_ = resp.Body.Close()
   646  			}
   647  			return nil, err
   648  		}
   649  
   650  		return readers.NewLimitedReadCloser(resp.Body, end-skip), nil
   651  	}
   652  
   653  	return resp.Body, nil
   654  }
   655  
   656  // Update in to the object with the modTime given of the given size
   657  //
   658  // When called from outside an Fs by rclone, src.Size() will always be >= 0.
   659  // But for unknown-sized objects (indicated by src.Size() == -1), Upload should either
   660  // return an error or update the object properly (rather than e.g. calling panic).
   661  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
   662  
   663  	srcRemote := o.Remote()
   664  
   665  	remote := path.Join(o.fs.root, srcRemote)
   666  	folderPath, fileName := path.Split(remote)
   667  
   668  	UseUniqueFileName := new(bool)
   669  	*UseUniqueFileName = false
   670  
   671  	var resp *client.UploadResult
   672  
   673  	err = o.fs.pacer.Call(func() (bool, error) {
   674  		var res *http.Response
   675  		res, resp, err = o.fs.ik.Upload(ctx, in, client.UploadParam{
   676  			FileName:      fileName,
   677  			Folder:        folderPath,
   678  			IsPrivateFile: o.file.IsPrivateFile,
   679  		})
   680  
   681  		return o.fs.shouldRetry(ctx, res, err)
   682  	})
   683  
   684  	if err != nil {
   685  		return err
   686  	}
   687  
   688  	fileID := resp.FileID
   689  
   690  	_, file, err := o.fs.ik.File(ctx, fileID)
   691  
   692  	if err != nil {
   693  		return err
   694  	}
   695  
   696  	o.file = *file
   697  
   698  	return nil
   699  }
   700  
   701  // Remove this object
   702  func (o *Object) Remove(ctx context.Context) (err error) {
   703  	err = o.fs.pacer.Call(func() (bool, error) {
   704  		var res *http.Response
   705  		res, err = o.fs.ik.DeleteFile(ctx, o.file.FileID)
   706  
   707  		return o.fs.shouldRetry(ctx, res, err)
   708  	})
   709  
   710  	return err
   711  }
   712  
   713  // SetModTime sets the metadata on the object to set the modification date
   714  func (o *Object) SetModTime(ctx context.Context, t time.Time) error {
   715  	return fs.ErrorCantSetModTime
   716  }
   717  
   718  func uploadFile(ctx context.Context, f *Fs, in io.Reader, srcRemote string, options ...fs.OpenOption) (fs.Object, error) {
   719  	remote := path.Join(f.root, srcRemote)
   720  	folderPath, fileName := path.Split(remote)
   721  
   722  	folderPath = f.EncodePath(folderPath)
   723  	fileName = f.EncodeFileName(fileName)
   724  
   725  	UseUniqueFileName := new(bool)
   726  	*UseUniqueFileName = false
   727  
   728  	err := f.pacer.Call(func() (bool, error) {
   729  		var res *http.Response
   730  		var err error
   731  		res, _, err = f.ik.Upload(ctx, in, client.UploadParam{
   732  			FileName:      fileName,
   733  			Folder:        folderPath,
   734  			IsPrivateFile: &f.opt.OnlySigned,
   735  		})
   736  
   737  		return f.shouldRetry(ctx, res, err)
   738  	})
   739  
   740  	if err != nil {
   741  		return nil, err
   742  	}
   743  
   744  	return f.NewObject(ctx, srcRemote)
   745  }
   746  
   747  // Metadata returns the metadata for the object
   748  func (o *Object) Metadata(ctx context.Context) (metadata fs.Metadata, err error) {
   749  
   750  	metadata.Set("btime", o.file.CreatedAt.Format(time.RFC3339))
   751  	metadata.Set("size", strconv.FormatUint(o.file.Size, 10))
   752  	metadata.Set("file-type", o.file.FileType)
   753  	metadata.Set("height", strconv.Itoa(o.file.Height))
   754  	metadata.Set("width", strconv.Itoa(o.file.Width))
   755  	metadata.Set("has-alpha", strconv.FormatBool(o.file.HasAlpha))
   756  
   757  	for k, v := range o.file.EmbeddedMetadata {
   758  		metadata.Set(k, fmt.Sprint(v))
   759  	}
   760  
   761  	if o.file.Tags != nil {
   762  		metadata.Set("tags", strings.Join(o.file.Tags, ","))
   763  	}
   764  
   765  	if o.file.CustomCoordinates != nil {
   766  		metadata.Set("custom-coordinates", *o.file.CustomCoordinates)
   767  	}
   768  
   769  	if o.file.IsPrivateFile != nil {
   770  		metadata.Set("is-private-file", strconv.FormatBool(*o.file.IsPrivateFile))
   771  	}
   772  
   773  	if o.file.AITags != nil {
   774  		googleTags := []string{}
   775  		awsTags := []string{}
   776  
   777  		for _, tag := range o.file.AITags {
   778  			if tag.Source == "google-auto-tagging" {
   779  				googleTags = append(googleTags, tag.Name)
   780  			} else if tag.Source == "aws-auto-tagging" {
   781  				awsTags = append(awsTags, tag.Name)
   782  			}
   783  		}
   784  
   785  		if len(googleTags) > 0 {
   786  			metadata.Set("google-tags", strings.Join(googleTags, ","))
   787  		}
   788  
   789  		if len(awsTags) > 0 {
   790  			metadata.Set("aws-tags", strings.Join(awsTags, ","))
   791  		}
   792  	}
   793  
   794  	return metadata, nil
   795  }
   796  
   797  // Copy src to this remote using server-side move operations.
   798  //
   799  // This is stored with the remote path given.
   800  //
   801  // It returns the destination Object and a possible error.
   802  //
   803  // Will only be called if src.Fs().Name() == f.Name()
   804  //
   805  // If it isn't possible then return fs.ErrorCantMove
   806  func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   807  	srcObj, ok := src.(*Object)
   808  	if !ok {
   809  		return nil, fs.ErrorCantMove
   810  	}
   811  
   812  	file, err := srcObj.Open(ctx)
   813  
   814  	if err != nil {
   815  		return nil, err
   816  	}
   817  
   818  	return uploadFile(ctx, f, file, remote)
   819  }
   820  
   821  // Check the interfaces are satisfied.
   822  var (
   823  	_ fs.Fs           = &Fs{}
   824  	_ fs.Purger       = &Fs{}
   825  	_ fs.PublicLinker = &Fs{}
   826  	_ fs.Object       = &Object{}
   827  	_ fs.Copier       = &Fs{}
   828  )