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