github.com/artpar/rclone@v1.67.3/backend/ulozto/ulozto.go (about)

     1  // Package ulozto provides an interface to the Uloz.to storage system.
     2  package ulozto
     3  
     4  import (
     5  	"bytes"
     6  	"context"
     7  	"encoding/base64"
     8  	"encoding/gob"
     9  	"encoding/hex"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"net/url"
    15  	"path"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/artpar/rclone/backend/ulozto/api"
    21  	"github.com/artpar/rclone/fs"
    22  	"github.com/artpar/rclone/fs/config"
    23  	"github.com/artpar/rclone/fs/config/configmap"
    24  	"github.com/artpar/rclone/fs/config/configstruct"
    25  	"github.com/artpar/rclone/fs/config/obscure"
    26  	"github.com/artpar/rclone/fs/fserrors"
    27  	"github.com/artpar/rclone/fs/fshttp"
    28  	"github.com/artpar/rclone/fs/hash"
    29  	"github.com/artpar/rclone/lib/dircache"
    30  	"github.com/artpar/rclone/lib/encoder"
    31  	"github.com/artpar/rclone/lib/pacer"
    32  	"github.com/artpar/rclone/lib/rest"
    33  )
    34  
    35  // TODO Uloz.to only supports file names of 255 characters or less and silently truncates names that are longer.
    36  
    37  const (
    38  	minSleep      = 10 * time.Millisecond
    39  	maxSleep      = 2 * time.Second
    40  	decayConstant = 2 // bigger for slower decay, exponential
    41  	rootURL       = "https://apis.uloz.to"
    42  )
    43  
    44  // Options defines the configuration for this backend
    45  type Options struct {
    46  	AppToken       string               `config:"app_token"`
    47  	Username       string               `config:"username"`
    48  	Password       string               `config:"password"`
    49  	RootFolderSlug string               `config:"root_folder_slug"`
    50  	Enc            encoder.MultiEncoder `config:"encoding"`
    51  	ListPageSize   int                  `config:"list_page_size"`
    52  }
    53  
    54  func init() {
    55  	fs.Register(&fs.RegInfo{
    56  		Name:        "ulozto",
    57  		Description: "Uloz.to",
    58  		NewFs:       NewFs,
    59  		Options: []fs.Option{
    60  			{
    61  				Name:    "app_token",
    62  				Default: "",
    63  				Help: `The application token identifying the app. An app API key can be either found in the API
    64  doc https://uloz.to/upload-resumable-api-beta or obtained from customer service.`,
    65  				Sensitive: true,
    66  			},
    67  			{
    68  				Name:      "username",
    69  				Default:   "",
    70  				Help:      "The username of the principal to operate as.",
    71  				Sensitive: true,
    72  			},
    73  			{
    74  				Name:       "password",
    75  				Default:    "",
    76  				Help:       "The password for the user.",
    77  				IsPassword: true,
    78  			},
    79  			{
    80  				Name: "root_folder_slug",
    81  				Help: `If set, rclone will use this folder as the root folder for all operations. For example,
    82  if the slug identifies 'foo/bar/', 'ulozto:baz' is equivalent to 'ulozto:foo/bar/baz' without
    83  any root slug set.`,
    84  				Default:   "",
    85  				Advanced:  true,
    86  				Sensitive: true,
    87  			},
    88  			{
    89  				Name:     "list_page_size",
    90  				Default:  500,
    91  				Help:     "The size of a single page for list commands. 1-500",
    92  				Advanced: true,
    93  			},
    94  			{
    95  				Name:     config.ConfigEncoding,
    96  				Help:     config.ConfigEncodingHelp,
    97  				Advanced: true,
    98  				Default:  encoder.Display | encoder.EncodeInvalidUtf8 | encoder.EncodeBackSlash,
    99  			},
   100  		}})
   101  }
   102  
   103  // Fs represents a remote uloz.to storage
   104  type Fs struct {
   105  	name     string             // name of this remote
   106  	root     string             // the path we are working on
   107  	opt      Options            // parsed options
   108  	features *fs.Features       // optional features
   109  	rest     *rest.Client       // REST client with authentication headers set, used to communicate with API endpoints
   110  	cdn      *rest.Client       // REST client without authentication headers set, used for CDN payload upload/download
   111  	dirCache *dircache.DirCache // Map of directory path to directory id
   112  	pacer    *fs.Pacer          // pacer for API calls
   113  }
   114  
   115  // NewFs constructs a Fs from the path, container:path
   116  func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
   117  	// Parse config into Options struct
   118  	opt := new(Options)
   119  	err := configstruct.Set(m, opt)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	client := fshttp.NewClient(ctx)
   125  
   126  	f := &Fs{
   127  		name:  name,
   128  		root:  root,
   129  		opt:   *opt,
   130  		cdn:   rest.NewClient(client),
   131  		rest:  rest.NewClient(client).SetRoot(rootURL),
   132  		pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
   133  	}
   134  	f.features = (&fs.Features{
   135  		DuplicateFiles:          true,
   136  		CanHaveEmptyDirectories: true,
   137  	}).Fill(ctx, f)
   138  	f.rest.SetErrorHandler(errorHandler)
   139  
   140  	f.rest.SetHeader("X-Auth-Token", f.opt.AppToken)
   141  
   142  	auth, err := f.authenticate(ctx)
   143  
   144  	if err != nil {
   145  		return f, err
   146  	}
   147  
   148  	var rootSlug string
   149  	if opt.RootFolderSlug == "" {
   150  		rootSlug = auth.Session.User.RootFolderSlug
   151  	} else {
   152  		rootSlug = opt.RootFolderSlug
   153  	}
   154  
   155  	f.dirCache = dircache.New(root, rootSlug, f)
   156  
   157  	err = f.dirCache.FindRoot(ctx, false)
   158  
   159  	if errors.Is(err, fs.ErrorDirNotFound) {
   160  		// All good, we'll create the folder later on.
   161  		return f, nil
   162  	}
   163  
   164  	if errors.Is(err, fs.ErrorIsFile) {
   165  		rootFolder, _ := dircache.SplitPath(root)
   166  		f.root = rootFolder
   167  		f.dirCache = dircache.New(rootFolder, rootSlug, f)
   168  		err = f.dirCache.FindRoot(ctx, false)
   169  		if err != nil {
   170  			return f, err
   171  		}
   172  		return f, fs.ErrorIsFile
   173  	}
   174  
   175  	return f, err
   176  }
   177  
   178  // errorHandler parses a non 2xx error response into an error
   179  func errorHandler(resp *http.Response) error {
   180  	// Decode error response
   181  	errResponse := new(api.Error)
   182  	err := rest.DecodeJSON(resp, &errResponse)
   183  	if err != nil {
   184  		fs.Debugf(nil, "Couldn't decode error response: %v", err)
   185  	}
   186  	if errResponse.StatusCode == 0 {
   187  		errResponse.StatusCode = resp.StatusCode
   188  	}
   189  	return errResponse
   190  }
   191  
   192  // retryErrorCodes is a slice of error codes that we will retry
   193  var retryErrorCodes = []int{
   194  	429, // Too Many Requests.
   195  	500, // Internal Server Error
   196  	502, // Bad Gateway
   197  	503, // Service Unavailable
   198  	504, // Gateway Timeout
   199  }
   200  
   201  // shouldRetry returns a boolean whether this resp and err should be retried.
   202  // It also returns the err for convenience.
   203  func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error, reauth bool) (bool, error) {
   204  	if err == nil {
   205  		return false, nil
   206  	}
   207  
   208  	if fserrors.ContextError(ctx, &err) {
   209  		return false, err
   210  	}
   211  
   212  	var apiErr *api.Error
   213  	if resp != nil && resp.StatusCode == 401 && errors.As(err, &apiErr) && apiErr.ErrorCode == 70001 {
   214  		fs.Debugf(nil, "Should retry: %v", err)
   215  
   216  		if reauth {
   217  			_, err = f.authenticate(ctx)
   218  			if err != nil {
   219  				return false, err
   220  			}
   221  		}
   222  
   223  		return true, err
   224  	}
   225  
   226  	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
   227  }
   228  
   229  func (f *Fs) authenticate(ctx context.Context) (response *api.AuthenticateResponse, err error) {
   230  	// TODO only reauth once if the token expires
   231  
   232  	// Remove the old user token
   233  	f.rest.RemoveHeader("X-User-Token")
   234  
   235  	opts := rest.Opts{
   236  		Method: "PUT",
   237  		Path:   "/v6/session",
   238  	}
   239  
   240  	clearPassword, err := obscure.Reveal(f.opt.Password)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  	authRequest := api.AuthenticateRequest{
   245  		Login:    f.opt.Username,
   246  		Password: clearPassword,
   247  	}
   248  
   249  	err = f.pacer.Call(func() (bool, error) {
   250  		httpResp, err := f.rest.CallJSON(ctx, &opts, &authRequest, &response)
   251  		return f.shouldRetry(ctx, httpResp, err, false)
   252  	})
   253  
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	f.rest.SetHeader("X-User-Token", response.TokenID)
   259  
   260  	return response, nil
   261  }
   262  
   263  // UploadSession represents a single Uloz.to upload session.
   264  //
   265  // Uloz.to supports uploading multiple files at once and committing them atomically. This functionality isn't being used
   266  // by the backend implementation and for simplicity, each session corresponds to a single file being uploaded.
   267  type UploadSession struct {
   268  	Filesystem  *Fs
   269  	URL         string
   270  	PrivateSlug string
   271  	ValidUntil  time.Time
   272  }
   273  
   274  func (f *Fs) createUploadSession(ctx context.Context) (session *UploadSession, err error) {
   275  	session = &UploadSession{
   276  		Filesystem: f,
   277  	}
   278  
   279  	err = session.renewUploadSession(ctx)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  
   284  	return session, nil
   285  }
   286  
   287  func (session *UploadSession) renewUploadSession(ctx context.Context) error {
   288  	opts := rest.Opts{
   289  		Method:     "POST",
   290  		Path:       "/v5/upload/link",
   291  		Parameters: url.Values{},
   292  	}
   293  
   294  	createUploadURLReq := api.CreateUploadURLRequest{
   295  		UserLogin: session.Filesystem.opt.Username,
   296  		Realm:     "ulozto",
   297  	}
   298  
   299  	if session.PrivateSlug != "" {
   300  		createUploadURLReq.ExistingSessionSlug = session.PrivateSlug
   301  	}
   302  
   303  	var err error
   304  	var response api.CreateUploadURLResponse
   305  
   306  	err = session.Filesystem.pacer.Call(func() (bool, error) {
   307  		httpResp, err := session.Filesystem.rest.CallJSON(ctx, &opts, &createUploadURLReq, &response)
   308  		return session.Filesystem.shouldRetry(ctx, httpResp, err, true)
   309  	})
   310  
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	session.PrivateSlug = response.PrivateSlug
   316  	session.URL = response.UploadURL
   317  	session.ValidUntil = response.ValidUntil
   318  
   319  	return nil
   320  }
   321  
   322  func (f *Fs) uploadUnchecked(ctx context.Context, name, parentSlug string, info fs.ObjectInfo, payload io.Reader) (fs.Object, error) {
   323  	session, err := f.createUploadSession(ctx)
   324  
   325  	if err != nil {
   326  		return nil, err
   327  	}
   328  
   329  	hashes := hash.NewHashSet(hash.MD5, hash.SHA256)
   330  	hasher, err := hash.NewMultiHasherTypes(hashes)
   331  
   332  	if err != nil {
   333  		return nil, err
   334  	}
   335  
   336  	payload = io.TeeReader(payload, hasher)
   337  
   338  	encodedName := f.opt.Enc.FromStandardName(name)
   339  
   340  	opts := rest.Opts{
   341  		Method: "POST",
   342  		Body:   payload,
   343  		// Not using Parameters as the session URL has parameters itself
   344  		RootURL:              session.URL + "&batch_file_id=1&is_porn=false",
   345  		MultipartContentName: "file",
   346  		MultipartFileName:    encodedName,
   347  		Parameters:           url.Values{},
   348  	}
   349  	if info.Size() > 0 {
   350  		size := info.Size()
   351  		opts.ContentLength = &size
   352  	}
   353  
   354  	var uploadResponse api.SendFilePayloadResponse
   355  
   356  	err = f.pacer.CallNoRetry(func() (bool, error) {
   357  		httpResp, err := f.cdn.CallJSON(ctx, &opts, nil, &uploadResponse)
   358  		return f.shouldRetry(ctx, httpResp, err, true)
   359  	})
   360  
   361  	if err != nil {
   362  		return nil, err
   363  	}
   364  
   365  	sha256digest, err := hasher.Sum(hash.SHA256)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  
   370  	md5digest, err := hasher.Sum(hash.MD5)
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  
   375  	if hex.EncodeToString(md5digest) != uploadResponse.Md5 {
   376  		return nil, errors.New("MD5 digest mismatch")
   377  	}
   378  
   379  	metadata := DescriptionEncodedMetadata{
   380  		Md5Hash:            md5digest,
   381  		Sha256Hash:         sha256digest,
   382  		ModTimeEpochMicros: info.ModTime(ctx).UnixMicro(),
   383  	}
   384  
   385  	encodedMetadata, err := metadata.encode()
   386  
   387  	if err != nil {
   388  		return nil, err
   389  	}
   390  
   391  	// Successfully uploaded, now move the file where it belongs and commit it
   392  	updateReq := api.BatchUpdateFilePropertiesRequest{
   393  		Name:         encodedName,
   394  		FolderSlug:   parentSlug,
   395  		Description:  encodedMetadata,
   396  		Slugs:        []string{uploadResponse.Slug},
   397  		UploadTokens: map[string]string{uploadResponse.Slug: session.PrivateSlug + ":1"},
   398  	}
   399  
   400  	var updateResponse []api.File
   401  
   402  	opts = rest.Opts{
   403  		Method:     "PATCH",
   404  		Path:       "/v8/file-list/private",
   405  		Parameters: url.Values{},
   406  	}
   407  
   408  	err = f.pacer.Call(func() (bool, error) {
   409  		httpResp, err := session.Filesystem.rest.CallJSON(ctx, &opts, &updateReq, &updateResponse)
   410  		return f.shouldRetry(ctx, httpResp, err, true)
   411  	})
   412  
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	if len(updateResponse) != 1 {
   418  		return nil, errors.New("unexpected number of files in the response")
   419  	}
   420  
   421  	opts = rest.Opts{
   422  		Method:     "PATCH",
   423  		Path:       "/v8/upload-batch/private/" + session.PrivateSlug,
   424  		Parameters: url.Values{},
   425  	}
   426  
   427  	commitRequest := api.CommitUploadBatchRequest{
   428  		Status:     "confirmed",
   429  		OwnerLogin: f.opt.Username,
   430  	}
   431  
   432  	var commitResponse api.CommitUploadBatchResponse
   433  
   434  	err = f.pacer.Call(func() (bool, error) {
   435  		httpResp, err := session.Filesystem.rest.CallJSON(ctx, &opts, &commitRequest, &commitResponse)
   436  		return f.shouldRetry(ctx, httpResp, err, true)
   437  	})
   438  
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	file, err := f.newObjectWithInfo(ctx, info.Remote(), &updateResponse[0])
   444  
   445  	return file, err
   446  }
   447  
   448  // Put implements the mandatory method fs.Fs.Put.
   449  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   450  	existingObj, err := f.NewObject(ctx, src.Remote())
   451  
   452  	switch {
   453  	case err == nil:
   454  		return existingObj, existingObj.Update(ctx, in, src, options...)
   455  	case errors.Is(err, fs.ErrorObjectNotFound):
   456  		// Not found so create it
   457  		return f.PutUnchecked(ctx, in, src, options...)
   458  	default:
   459  		return nil, err
   460  	}
   461  }
   462  
   463  // PutUnchecked implements the optional interface fs.PutUncheckeder.
   464  //
   465  // Uloz.to allows to have multiple files of the same name in the same folder.
   466  func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   467  	filename, folderSlug, err := f.dirCache.FindPath(ctx, src.Remote(), true)
   468  
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  
   473  	return f.uploadUnchecked(ctx, filename, folderSlug, src, in)
   474  }
   475  
   476  // Mkdir implements the mandatory method fs.Fs.Mkdir.
   477  func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
   478  	_, err = f.dirCache.FindDir(ctx, dir, true)
   479  	return err
   480  }
   481  
   482  func (f *Fs) isDirEmpty(ctx context.Context, slug string) (empty bool, err error) {
   483  	folders, err := f.fetchListFolderPage(ctx, slug, "", 1, 0)
   484  
   485  	if err != nil {
   486  		return false, err
   487  	}
   488  
   489  	if len(folders) > 0 {
   490  		return false, nil
   491  	}
   492  
   493  	files, err := f.fetchListFilePage(ctx, slug, "", 1, 0)
   494  
   495  	if err != nil {
   496  		return false, err
   497  	}
   498  
   499  	if len(files) > 0 {
   500  		return false, nil
   501  	}
   502  
   503  	return true, nil
   504  }
   505  
   506  // Rmdir implements the mandatory method fs.Fs.Rmdir.
   507  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   508  	slug, err := f.dirCache.FindDir(ctx, dir, false)
   509  
   510  	if err != nil {
   511  		return err
   512  	}
   513  
   514  	empty, err := f.isDirEmpty(ctx, slug)
   515  
   516  	if err != nil {
   517  		return err
   518  	}
   519  
   520  	if !empty {
   521  		return fs.ErrorDirectoryNotEmpty
   522  	}
   523  
   524  	opts := rest.Opts{
   525  		Method: "DELETE",
   526  		Path:   "/v5/user/" + f.opt.Username + "/folder-list",
   527  	}
   528  
   529  	req := api.DeleteFoldersRequest{Slugs: []string{slug}}
   530  	err = f.pacer.Call(func() (bool, error) {
   531  		httpResp, err := f.rest.CallJSON(ctx, &opts, req, nil)
   532  		return f.shouldRetry(ctx, httpResp, err, true)
   533  	})
   534  
   535  	if err != nil {
   536  		return err
   537  	}
   538  
   539  	f.dirCache.FlushDir(dir)
   540  
   541  	return nil
   542  }
   543  
   544  // Move implements the optional method fs.Mover.Move.
   545  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   546  	if remote == src.Remote() {
   547  		// Already there, do nothing
   548  		return src, nil
   549  	}
   550  
   551  	srcObj, ok := src.(*Object)
   552  	if !ok {
   553  		fs.Debugf(src, "Can't move - not same remote type")
   554  		return nil, fs.ErrorCantMove
   555  	}
   556  
   557  	filename, folderSlug, err := f.dirCache.FindPath(ctx, remote, true)
   558  
   559  	if err != nil {
   560  		return nil, err
   561  	}
   562  
   563  	newObj := &Object{}
   564  	newObj.copyFrom(srcObj)
   565  	newObj.remote = remote
   566  
   567  	return newObj, newObj.updateFileProperties(ctx, api.MoveFileRequest{
   568  		ParentFolderSlug: folderSlug,
   569  		NewFilename:      filename,
   570  	})
   571  }
   572  
   573  // DirMove implements the optional method fs.DirMover.DirMove.
   574  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
   575  	srcFs, ok := src.(*Fs)
   576  	if !ok {
   577  		fs.Debugf(srcFs, "Can't move directory - not same remote type")
   578  		return fs.ErrorCantDirMove
   579  	}
   580  
   581  	srcSlug, _, srcName, dstParentSlug, dstName, err := f.dirCache.DirMove(ctx, srcFs.dirCache, srcFs.root, srcRemote, f.root, dstRemote)
   582  	if err != nil {
   583  		return err
   584  	}
   585  
   586  	opts := rest.Opts{
   587  		Method: "PATCH",
   588  		Path:   "/v6/user/" + f.opt.Username + "/folder-list/parent-folder",
   589  	}
   590  
   591  	req := api.MoveFolderRequest{
   592  		FolderSlugs:         []string{srcSlug},
   593  		NewParentFolderSlug: dstParentSlug,
   594  	}
   595  
   596  	err = f.pacer.Call(func() (bool, error) {
   597  		httpResp, err := f.rest.CallJSON(ctx, &opts, &req, nil)
   598  		return f.shouldRetry(ctx, httpResp, err, true)
   599  	})
   600  
   601  	if err != nil {
   602  		return err
   603  	}
   604  
   605  	// The old folder doesn't exist anymore so clear the cache now instead of after renaming
   606  	srcFs.dirCache.FlushDir(srcRemote)
   607  
   608  	if srcName != dstName {
   609  		// There's no endpoint to rename the folder alongside moving it, so this has to happen separately.
   610  		opts = rest.Opts{
   611  			Method: "PATCH",
   612  			Path:   "/v7/user/" + f.opt.Username + "/folder/" + srcSlug,
   613  		}
   614  
   615  		renameReq := api.RenameFolderRequest{
   616  			NewName: dstName,
   617  		}
   618  
   619  		err = f.pacer.Call(func() (bool, error) {
   620  			httpResp, err := f.rest.CallJSON(ctx, &opts, &renameReq, nil)
   621  			return f.shouldRetry(ctx, httpResp, err, true)
   622  		})
   623  
   624  		return err
   625  	}
   626  
   627  	return nil
   628  }
   629  
   630  // Name of the remote (as passed into NewFs)
   631  func (f *Fs) Name() string {
   632  	return f.name
   633  }
   634  
   635  // Root of the remote (as passed into NewFs)
   636  func (f *Fs) Root() string {
   637  	return f.root
   638  }
   639  
   640  // String converts this Fs to a string
   641  func (f *Fs) String() string {
   642  	return fmt.Sprintf("uloz.to root '%s'", f.root)
   643  }
   644  
   645  // Features returns the optional features of this Fs
   646  func (f *Fs) Features() *fs.Features {
   647  	return f.features
   648  }
   649  
   650  // Precision return the precision of this Fs
   651  func (f *Fs) Precision() time.Duration {
   652  	return time.Microsecond
   653  }
   654  
   655  // Hashes implements fs.Fs.Hashes by returning the supported hash types of the filesystem.
   656  func (f *Fs) Hashes() hash.Set {
   657  	return hash.NewHashSet(hash.SHA256, hash.MD5)
   658  }
   659  
   660  // DescriptionEncodedMetadata represents a set of metadata encoded as Uloz.to description.
   661  //
   662  // Uloz.to doesn't support setting metadata such as mtime but allows the user to set an arbitrary description field.
   663  // The content of this structure will be serialized and stored in the backend.
   664  //
   665  // The files themselves are immutable so there's no danger that the file changes, and we'll forget to update the hashes.
   666  // It is theoretically possible to rewrite the description to provide incorrect information for a file. However, in case
   667  // it's a real attack vector, a nefarious person already has write access to the repo, and the situation is above
   668  // rclone's pay grade already.
   669  type DescriptionEncodedMetadata struct {
   670  	Md5Hash            []byte // The MD5 hash of the file
   671  	Sha256Hash         []byte // The SHA256 hash of the file
   672  	ModTimeEpochMicros int64  // The mtime of the file, as set by rclone
   673  }
   674  
   675  func (md *DescriptionEncodedMetadata) encode() (string, error) {
   676  	b := bytes.Buffer{}
   677  	e := gob.NewEncoder(&b)
   678  	err := e.Encode(md)
   679  	if err != nil {
   680  		return "", err
   681  	}
   682  	// Version the encoded string from the beginning even though we don't need it yet.
   683  	return "1;" + base64.StdEncoding.EncodeToString(b.Bytes()), nil
   684  }
   685  
   686  func decodeDescriptionMetadata(str string) (*DescriptionEncodedMetadata, error) {
   687  	// The encoded data starts with a version number which is not a part iof the serialized object
   688  	spl := strings.SplitN(str, ";", 2)
   689  
   690  	if len(spl) < 2 || spl[0] != "1" {
   691  		return nil, errors.New("can't decode, unknown encoded metadata version")
   692  	}
   693  
   694  	m := DescriptionEncodedMetadata{}
   695  	by, err := base64.StdEncoding.DecodeString(spl[1])
   696  	if err != nil {
   697  		return nil, err
   698  	}
   699  	b := bytes.Buffer{}
   700  	b.Write(by)
   701  	d := gob.NewDecoder(&b)
   702  	err = d.Decode(&m)
   703  	if err != nil {
   704  		return nil, err
   705  	}
   706  	return &m, nil
   707  }
   708  
   709  // Object describes an uloz.to object.
   710  //
   711  // Valid objects will always have all fields but encodedMetadata set.
   712  type Object struct {
   713  	fs            *Fs       // what this object is part of
   714  	remote        string    // The remote path
   715  	name          string    // The file name
   716  	size          int64     // size of the object
   717  	slug          string    // ID of the object
   718  	remoteFsMtime time.Time // The time the object was last modified in the remote fs.
   719  	// Metadata not available natively and encoded in the description field. May not be present if the encoded metadata
   720  	// is not present (e.g. if file wasn't uploaded by rclone) or invalid.
   721  	encodedMetadata *DescriptionEncodedMetadata
   722  }
   723  
   724  // Storable implements the mandatory method fs.ObjectInfo.Storable
   725  func (o *Object) Storable() bool {
   726  	return true
   727  }
   728  
   729  func (o *Object) updateFileProperties(ctx context.Context, req interface{}) (err error) {
   730  	var resp *api.File
   731  
   732  	opts := rest.Opts{
   733  		Method: "PATCH",
   734  		Path:   "/v8/file/" + o.slug + "/private",
   735  	}
   736  
   737  	err = o.fs.pacer.Call(func() (bool, error) {
   738  		httpResp, err := o.fs.rest.CallJSON(ctx, &opts, &req, &resp)
   739  		return o.fs.shouldRetry(ctx, httpResp, err, true)
   740  	})
   741  
   742  	if err != nil {
   743  		return err
   744  	}
   745  
   746  	return o.setMetaData(resp)
   747  }
   748  
   749  // SetModTime implements the mandatory method fs.Object.SetModTime
   750  func (o *Object) SetModTime(ctx context.Context, t time.Time) (err error) {
   751  	var newMetadata DescriptionEncodedMetadata
   752  	if o.encodedMetadata == nil {
   753  		newMetadata = DescriptionEncodedMetadata{}
   754  	} else {
   755  		newMetadata = *o.encodedMetadata
   756  	}
   757  
   758  	newMetadata.ModTimeEpochMicros = t.UnixMicro()
   759  	encoded, err := newMetadata.encode()
   760  	if err != nil {
   761  		return err
   762  	}
   763  	return o.updateFileProperties(ctx, api.UpdateDescriptionRequest{
   764  		Description: encoded,
   765  	})
   766  }
   767  
   768  // Open implements the mandatory method fs.Object.Open
   769  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (rc io.ReadCloser, err error) {
   770  	opts := rest.Opts{
   771  		Method: "POST",
   772  		Path:   "/v5/file/download-link/vipdata",
   773  	}
   774  
   775  	req := &api.GetDownloadLinkRequest{
   776  		Slug:      o.slug,
   777  		UserLogin: o.fs.opt.Username,
   778  		// Has to be set but doesn't seem to be used server side.
   779  		DeviceID: "foobar",
   780  	}
   781  
   782  	var resp *api.GetDownloadLinkResponse
   783  
   784  	err = o.fs.pacer.Call(func() (bool, error) {
   785  		httpResp, err := o.fs.rest.CallJSON(ctx, &opts, &req, &resp)
   786  		return o.fs.shouldRetry(ctx, httpResp, err, true)
   787  	})
   788  	if err != nil {
   789  		return nil, err
   790  	}
   791  
   792  	opts = rest.Opts{
   793  		Method:  "GET",
   794  		RootURL: resp.Link,
   795  		Options: options,
   796  	}
   797  
   798  	var httpResp *http.Response
   799  
   800  	err = o.fs.pacer.Call(func() (bool, error) {
   801  		httpResp, err = o.fs.cdn.Call(ctx, &opts)
   802  		return o.fs.shouldRetry(ctx, httpResp, err, true)
   803  	})
   804  	if err != nil {
   805  		return nil, err
   806  	}
   807  	return httpResp.Body, err
   808  }
   809  
   810  func (o *Object) copyFrom(other *Object) {
   811  	o.fs = other.fs
   812  	o.remote = other.remote
   813  	o.size = other.size
   814  	o.slug = other.slug
   815  	o.remoteFsMtime = other.remoteFsMtime
   816  	o.encodedMetadata = other.encodedMetadata
   817  }
   818  
   819  // RenamingObjectInfoProxy is a delegating proxy for fs.ObjectInfo
   820  // with the option of specifying a different remote path.
   821  type RenamingObjectInfoProxy struct {
   822  	delegate fs.ObjectInfo
   823  	remote   string
   824  }
   825  
   826  // Remote implements fs.ObjectInfo.Remote by delegating to the wrapped instance.
   827  func (s *RenamingObjectInfoProxy) String() string {
   828  	return s.delegate.String()
   829  }
   830  
   831  // Remote implements fs.ObjectInfo.Remote by returning the specified remote path.
   832  func (s *RenamingObjectInfoProxy) Remote() string {
   833  	return s.remote
   834  }
   835  
   836  // ModTime implements fs.ObjectInfo.ModTime by delegating to the wrapped instance.
   837  func (s *RenamingObjectInfoProxy) ModTime(ctx context.Context) time.Time {
   838  	return s.delegate.ModTime(ctx)
   839  }
   840  
   841  // Size implements fs.ObjectInfo.Size by delegating to the wrapped instance.
   842  func (s *RenamingObjectInfoProxy) Size() int64 {
   843  	return s.delegate.Size()
   844  }
   845  
   846  // Fs implements fs.ObjectInfo.Fs by delegating to the wrapped instance.
   847  func (s *RenamingObjectInfoProxy) Fs() fs.Info {
   848  	return s.delegate.Fs()
   849  }
   850  
   851  // Hash implements fs.ObjectInfo.Hash by delegating to the wrapped instance.
   852  func (s *RenamingObjectInfoProxy) Hash(ctx context.Context, ty hash.Type) (string, error) {
   853  	return s.delegate.Hash(ctx, ty)
   854  }
   855  
   856  // Storable implements fs.ObjectInfo.Storable by delegating to the wrapped instance.
   857  func (s *RenamingObjectInfoProxy) Storable() bool {
   858  	return s.delegate.Storable()
   859  }
   860  
   861  // Update implements the mandatory method fs.Object.Update
   862  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
   863  	// The backend allows to store multiple files with the same name, so simply upload the new file and remove the old
   864  	// one afterwards.
   865  	info := &RenamingObjectInfoProxy{
   866  		delegate: src,
   867  		remote:   o.Remote(),
   868  	}
   869  	newo, err := o.fs.PutUnchecked(ctx, in, info, options...)
   870  
   871  	if err != nil {
   872  		return err
   873  	}
   874  
   875  	err = o.Remove(ctx)
   876  	if err != nil {
   877  		return err
   878  	}
   879  
   880  	o.copyFrom(newo.(*Object))
   881  
   882  	return nil
   883  }
   884  
   885  // Remove implements the mandatory method fs.Object.Remove
   886  func (o *Object) Remove(ctx context.Context) error {
   887  	for i := 0; i < 2; i++ {
   888  		// First call moves the item to recycle bin, second deletes it for good
   889  		var err error
   890  		opts := rest.Opts{
   891  			Method: "DELETE",
   892  			Path:   "/v6/file/" + o.slug + "/private",
   893  		}
   894  		err = o.fs.pacer.Call(func() (bool, error) {
   895  			httpResp, err := o.fs.rest.CallJSON(ctx, &opts, nil, nil)
   896  			return o.fs.shouldRetry(ctx, httpResp, err, true)
   897  		})
   898  		if err != nil {
   899  			return err
   900  		}
   901  	}
   902  
   903  	return nil
   904  }
   905  
   906  // ModTime implements the mandatory method fs.Object.ModTime
   907  func (o *Object) ModTime(ctx context.Context) time.Time {
   908  	if o.encodedMetadata != nil {
   909  		return time.UnixMicro(o.encodedMetadata.ModTimeEpochMicros)
   910  	}
   911  
   912  	// The time the object was last modified on the server - a handwavy guess, but we don't have any better
   913  	return o.remoteFsMtime
   914  
   915  }
   916  
   917  // Fs implements the mandatory method fs.Object.Fs
   918  func (o *Object) Fs() fs.Info {
   919  	return o.fs
   920  }
   921  
   922  // String returns the string representation of the remote object reference.
   923  func (o *Object) String() string {
   924  	if o == nil {
   925  		return "<nil>"
   926  	}
   927  	return o.remote
   928  }
   929  
   930  // Remote returns the remote path
   931  func (o *Object) Remote() string {
   932  	return o.remote
   933  }
   934  
   935  // Size returns the size of an object in bytes
   936  func (o *Object) Size() int64 {
   937  	return o.size
   938  }
   939  
   940  // Hash implements the mandatory method fs.Object.Hash.
   941  //
   942  // Supports SHA256 and MD5 hashes.
   943  func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
   944  	if t != hash.MD5 && t != hash.SHA256 {
   945  		return "", hash.ErrUnsupported
   946  	}
   947  
   948  	if o.encodedMetadata == nil {
   949  		return "", nil
   950  	}
   951  
   952  	switch t {
   953  	case hash.MD5:
   954  		return hex.EncodeToString(o.encodedMetadata.Md5Hash), nil
   955  	case hash.SHA256:
   956  		return hex.EncodeToString(o.encodedMetadata.Sha256Hash), nil
   957  	}
   958  
   959  	panic("Should never get here")
   960  }
   961  
   962  // FindLeaf implements dircache.DirCacher.FindLeaf by successively walking through the folder hierarchy until
   963  // the desired folder is found, or there's nowhere to continue.
   964  func (f *Fs) FindLeaf(ctx context.Context, folderSlug, leaf string) (leafSlug string, found bool, err error) {
   965  	folders, err := f.listFolders(ctx, folderSlug, leaf)
   966  	if err != nil {
   967  		if errors.Is(err, fs.ErrorDirNotFound) {
   968  			return "", false, nil
   969  		}
   970  		return "", false, err
   971  	}
   972  
   973  	for _, folder := range folders {
   974  		if folder.Name == leaf {
   975  			return folder.Slug, true, nil
   976  		}
   977  	}
   978  
   979  	// Uloz.to allows creation of multiple files / folders with the same name in the same parent folder. rclone always
   980  	// expects folder paths to be unique (no other file or folder with the same name should exist). As a result we also
   981  	// need to look at the files to return the correct error if necessary.
   982  	files, err := f.listFiles(ctx, folderSlug, leaf)
   983  	if err != nil {
   984  		return "", false, err
   985  	}
   986  
   987  	for _, file := range files {
   988  		if file.Name == leaf {
   989  			return "", false, fs.ErrorIsFile
   990  		}
   991  	}
   992  
   993  	// The parent folder exists but no file or folder with the given name was found in it.
   994  	return "", false, nil
   995  }
   996  
   997  // CreateDir implements dircache.DirCacher.CreateDir by creating a folder with the given name under a folder identified
   998  // by parentSlug.
   999  func (f *Fs) CreateDir(ctx context.Context, parentSlug, leaf string) (newID string, err error) {
  1000  	var folder *api.Folder
  1001  	opts := rest.Opts{
  1002  		Method:     "POST",
  1003  		Path:       "/v6/user/" + f.opt.Username + "/folder",
  1004  		Parameters: url.Values{},
  1005  	}
  1006  	mkdir := api.CreateFolderRequest{
  1007  		Name:             f.opt.Enc.FromStandardName(leaf),
  1008  		ParentFolderSlug: parentSlug,
  1009  	}
  1010  	err = f.pacer.Call(func() (bool, error) {
  1011  		httpResp, err := f.rest.CallJSON(ctx, &opts, &mkdir, &folder)
  1012  		return f.shouldRetry(ctx, httpResp, err, true)
  1013  	})
  1014  	if err != nil {
  1015  		return "", err
  1016  	}
  1017  	return folder.Slug, nil
  1018  }
  1019  
  1020  func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.File) (*Object, error) {
  1021  	o := &Object{
  1022  		fs:     f,
  1023  		remote: remote,
  1024  	}
  1025  	var err error
  1026  
  1027  	if info == nil {
  1028  		info, err = f.readMetaDataForPath(ctx, remote)
  1029  	}
  1030  
  1031  	if err != nil {
  1032  		return nil, err
  1033  	}
  1034  
  1035  	err = o.setMetaData(info)
  1036  	if err != nil {
  1037  		return nil, err
  1038  	}
  1039  	return o, nil
  1040  }
  1041  
  1042  // readMetaDataForPath reads the metadata from the path
  1043  func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.File, err error) {
  1044  	filename, folderSlug, err := f.dirCache.FindPath(ctx, path, false)
  1045  	if err != nil {
  1046  		if errors.Is(err, fs.ErrorDirNotFound) {
  1047  			return nil, fs.ErrorObjectNotFound
  1048  		}
  1049  		return nil, err
  1050  	}
  1051  
  1052  	files, err := f.listFiles(ctx, folderSlug, filename)
  1053  
  1054  	if err != nil {
  1055  		return nil, err
  1056  	}
  1057  
  1058  	for _, file := range files {
  1059  		if file.Name == filename {
  1060  			return &file, nil
  1061  		}
  1062  	}
  1063  
  1064  	folders, err := f.listFolders(ctx, folderSlug, filename)
  1065  
  1066  	if err != nil {
  1067  		return nil, err
  1068  	}
  1069  
  1070  	for _, file := range folders {
  1071  		if file.Name == filename {
  1072  			return nil, fs.ErrorIsDir
  1073  		}
  1074  	}
  1075  
  1076  	return nil, fs.ErrorObjectNotFound
  1077  }
  1078  
  1079  func (o *Object) setMetaData(info *api.File) (err error) {
  1080  	o.name = info.Name
  1081  	o.size = info.Filesize
  1082  	o.remoteFsMtime = info.LastUserModified
  1083  	o.encodedMetadata, err = decodeDescriptionMetadata(info.Description)
  1084  	if err != nil {
  1085  		fs.Debugf(o, "Couldn't decode metadata: %v", err)
  1086  	}
  1087  	o.slug = info.Slug
  1088  	return nil
  1089  }
  1090  
  1091  // NewObject implements fs.Fs.NewObject.
  1092  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
  1093  	return f.newObjectWithInfo(ctx, remote, nil)
  1094  }
  1095  
  1096  // List implements fs.Fs.List by listing all files and folders in the given folder.
  1097  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
  1098  	folderSlug, err := f.dirCache.FindDir(ctx, dir, false)
  1099  	if err != nil {
  1100  		return nil, err
  1101  	}
  1102  
  1103  	folders, err := f.listFolders(ctx, folderSlug, "")
  1104  	if err != nil {
  1105  		return nil, err
  1106  	}
  1107  
  1108  	for _, folder := range folders {
  1109  		remote := path.Join(dir, folder.Name)
  1110  		f.dirCache.Put(remote, folder.Slug)
  1111  		entries = append(entries, fs.NewDir(remote, folder.LastUserModified))
  1112  	}
  1113  
  1114  	files, err := f.listFiles(ctx, folderSlug, "")
  1115  	if err != nil {
  1116  		return nil, err
  1117  	}
  1118  
  1119  	for _, file := range files {
  1120  		remote := path.Join(dir, file.Name)
  1121  		remoteFile, err := f.newObjectWithInfo(ctx, remote, &file)
  1122  		if err != nil {
  1123  			return nil, err
  1124  		}
  1125  		entries = append(entries, remoteFile)
  1126  	}
  1127  
  1128  	return entries, nil
  1129  }
  1130  
  1131  func (f *Fs) fetchListFolderPage(
  1132  	ctx context.Context,
  1133  	folderSlug string,
  1134  	searchQuery string,
  1135  	limit int,
  1136  	offset int) (folders []api.Folder, err error) {
  1137  
  1138  	opts := rest.Opts{
  1139  		Method:     "GET",
  1140  		Path:       "/v9/user/" + f.opt.Username + "/folder/" + folderSlug + "/folder-list",
  1141  		Parameters: url.Values{},
  1142  	}
  1143  
  1144  	opts.Parameters.Set("status", "ok")
  1145  	opts.Parameters.Set("limit", strconv.Itoa(limit))
  1146  	if offset > 0 {
  1147  		opts.Parameters.Set("offset", strconv.Itoa(offset))
  1148  	}
  1149  
  1150  	if searchQuery != "" {
  1151  		opts.Parameters.Set("search_query", f.opt.Enc.FromStandardName(searchQuery))
  1152  	}
  1153  
  1154  	var respBody *api.ListFoldersResponse
  1155  
  1156  	err = f.pacer.Call(func() (bool, error) {
  1157  		httpResp, err := f.rest.CallJSON(ctx, &opts, nil, &respBody)
  1158  		return f.shouldRetry(ctx, httpResp, err, true)
  1159  	})
  1160  
  1161  	if err != nil {
  1162  		return nil, err
  1163  	}
  1164  
  1165  	for i := range respBody.Subfolders {
  1166  		respBody.Subfolders[i].Name = f.opt.Enc.ToStandardName(respBody.Subfolders[i].Name)
  1167  	}
  1168  
  1169  	return respBody.Subfolders, nil
  1170  }
  1171  
  1172  func (f *Fs) listFolders(
  1173  	ctx context.Context,
  1174  	folderSlug string,
  1175  	searchQuery string) (folders []api.Folder, err error) {
  1176  
  1177  	targetPageSize := f.opt.ListPageSize
  1178  	lastPageSize := targetPageSize
  1179  	offset := 0
  1180  
  1181  	for targetPageSize == lastPageSize {
  1182  		page, err := f.fetchListFolderPage(ctx, folderSlug, searchQuery, targetPageSize, offset)
  1183  		if err != nil {
  1184  			var apiErr *api.Error
  1185  			casted := errors.As(err, &apiErr)
  1186  			if casted && apiErr.ErrorCode == 30001 {
  1187  				return nil, fs.ErrorDirNotFound
  1188  			}
  1189  			return nil, err
  1190  		}
  1191  		lastPageSize = len(page)
  1192  		offset += lastPageSize
  1193  		folders = append(folders, page...)
  1194  	}
  1195  
  1196  	return folders, nil
  1197  }
  1198  
  1199  func (f *Fs) fetchListFilePage(
  1200  	ctx context.Context,
  1201  	folderSlug string,
  1202  	searchQuery string,
  1203  	limit int,
  1204  	offset int) (folders []api.File, err error) {
  1205  
  1206  	opts := rest.Opts{
  1207  		Method:     "GET",
  1208  		Path:       "/v8/user/" + f.opt.Username + "/folder/" + folderSlug + "/file-list",
  1209  		Parameters: url.Values{},
  1210  	}
  1211  	opts.Parameters.Set("status", "ok")
  1212  	opts.Parameters.Set("limit", strconv.Itoa(limit))
  1213  	if offset > 0 {
  1214  		opts.Parameters.Set("offset", strconv.Itoa(offset))
  1215  	}
  1216  
  1217  	if searchQuery != "" {
  1218  		opts.Parameters.Set("search_query", f.opt.Enc.FromStandardName(searchQuery))
  1219  	}
  1220  
  1221  	var respBody *api.ListFilesResponse
  1222  
  1223  	err = f.pacer.Call(func() (bool, error) {
  1224  		httpResp, err := f.rest.CallJSON(ctx, &opts, nil, &respBody)
  1225  		return f.shouldRetry(ctx, httpResp, err, true)
  1226  	})
  1227  
  1228  	if err != nil {
  1229  		return nil, fmt.Errorf("couldn't list files: %w", err)
  1230  	}
  1231  
  1232  	for i := range respBody.Items {
  1233  		respBody.Items[i].Name = f.opt.Enc.ToStandardName(respBody.Items[i].Name)
  1234  	}
  1235  
  1236  	return respBody.Items, nil
  1237  }
  1238  
  1239  func (f *Fs) listFiles(
  1240  	ctx context.Context,
  1241  	folderSlug string,
  1242  	searchQuery string) (folders []api.File, err error) {
  1243  
  1244  	targetPageSize := f.opt.ListPageSize
  1245  	lastPageSize := targetPageSize
  1246  	offset := 0
  1247  
  1248  	for targetPageSize == lastPageSize {
  1249  		page, err := f.fetchListFilePage(ctx, folderSlug, searchQuery, targetPageSize, offset)
  1250  		if err != nil {
  1251  			return nil, err
  1252  		}
  1253  		lastPageSize = len(page)
  1254  		offset += lastPageSize
  1255  		folders = append(folders, page...)
  1256  	}
  1257  
  1258  	return folders, nil
  1259  }
  1260  
  1261  // DirCacheFlush implements the optional fs.DirCacheFlusher interface.
  1262  func (f *Fs) DirCacheFlush() {
  1263  	f.dirCache.ResetRoot()
  1264  }
  1265  
  1266  // Check the interfaces are satisfied
  1267  var (
  1268  	_ fs.Fs              = (*Fs)(nil)
  1269  	_ dircache.DirCacher = (*Fs)(nil)
  1270  	_ fs.DirCacheFlusher = (*Fs)(nil)
  1271  	_ fs.PutUncheckeder  = (*Fs)(nil)
  1272  	_ fs.Mover           = (*Fs)(nil)
  1273  	_ fs.DirMover        = (*Fs)(nil)
  1274  	_ fs.Object          = (*Object)(nil)
  1275  	_ fs.ObjectInfo      = (*RenamingObjectInfoProxy)(nil)
  1276  )