12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088 |
- // Package uptobox provides an interface to the Uptobox storage system.
- package uptobox
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "path"
- "regexp"
- "strconv"
- "strings"
- "time"
- "github.com/rclone/rclone/backend/uptobox/api"
- "github.com/rclone/rclone/fs"
- "github.com/rclone/rclone/fs/config"
- "github.com/rclone/rclone/fs/config/configmap"
- "github.com/rclone/rclone/fs/config/configstruct"
- "github.com/rclone/rclone/fs/fserrors"
- "github.com/rclone/rclone/fs/fshttp"
- "github.com/rclone/rclone/fs/hash"
- "github.com/rclone/rclone/lib/encoder"
- "github.com/rclone/rclone/lib/pacer"
- "github.com/rclone/rclone/lib/random"
- "github.com/rclone/rclone/lib/rest"
- )
- const (
- apiBaseURL = "https://uptobox.com/api"
- minSleep = 400 * time.Millisecond // api is extremely rate limited now
- maxSleep = 5 * time.Second
- decayConstant = 2 // bigger for slower decay, exponential
- attackConstant = 0 // start with max sleep
- )
- func init() {
- fs.Register(&fs.RegInfo{
- Name: "uptobox",
- Description: "Uptobox",
- NewFs: NewFs,
- Options: []fs.Option{{
- Help: "Your access token.\n\nGet it from https://uptobox.com/my_account.",
- Name: "access_token",
- Sensitive: true,
- }, {
- Help: "Set to make uploaded files private",
- Name: "private",
- Advanced: true,
- Default: false,
- }, {
- Name: config.ConfigEncoding,
- Help: config.ConfigEncodingHelp,
- Advanced: true,
- // maxFileLength = 255
- Default: (encoder.Display |
- encoder.EncodeBackQuote |
- encoder.EncodeDoubleQuote |
- encoder.EncodeLtGt |
- encoder.EncodeLeftSpace |
- encoder.EncodeInvalidUtf8),
- }},
- })
- }
- // Options defines the configuration for this backend
- type Options struct {
- AccessToken string `config:"access_token"`
- Private bool `config:"private"`
- Enc encoder.MultiEncoder `config:"encoding"`
- }
- // Fs is the interface a cloud storage system must provide
- type Fs struct {
- root string
- name string
- opt Options
- features *fs.Features
- srv *rest.Client
- pacer *fs.Pacer
- IDRegexp *regexp.Regexp
- public string // "0" to make objects private
- }
- // Object represents an Uptobox object
- type Object struct {
- fs *Fs // what this object is part of
- remote string // The remote path
- hasMetaData bool // whether info below has been set
- size int64 // Bytes in the object
- // modTime time.Time // Modified time of the object
- code string
- }
- // Name of the remote (as passed into NewFs)
- func (f *Fs) Name() string {
- return f.name
- }
- // Root of the remote (as passed into NewFs)
- func (f *Fs) Root() string {
- return f.root
- }
- // String returns a description of the FS
- func (f *Fs) String() string {
- return fmt.Sprintf("Uptobox root '%s'", f.root)
- }
- // Precision of the ModTimes in this Fs
- func (f *Fs) Precision() time.Duration {
- return fs.ModTimeNotSupported
- }
- // Hashes returns the supported hash types of the filesystem
- func (f *Fs) Hashes() hash.Set {
- return hash.Set(hash.None)
- }
- // Features returns the optional features of this Fs
- func (f *Fs) Features() *fs.Features {
- return f.features
- }
- // retryErrorCodes is a slice of error codes that we will retry
- var retryErrorCodes = []int{
- 429, // Too Many Requests.
- 500, // Internal Server Error
- 502, // Bad Gateway
- 503, // Service Unavailable
- 504, // Gateway Timeout
- 509, // Bandwidth Limit Exceeded
- }
- // shouldRetry returns a boolean as to whether this resp and err
- // deserve to be retried. It returns the err as a convenience
- func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
- if fserrors.ContextError(ctx, &err) {
- return false, err
- }
- return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
- }
- // dirPath returns an escaped file path (f.root, file)
- func (f *Fs) dirPath(file string) string {
- //return path.Join(f.diskRoot, file)
- if file == "" || file == "." {
- return "//" + f.root
- }
- return "//" + path.Join(f.root, file)
- }
- // returns the full path based on root and the last element
- func (f *Fs) splitPathFull(pth string) (string, string) {
- fullPath := strings.Trim(path.Join(f.root, pth), "/")
- i := len(fullPath) - 1
- for i >= 0 && fullPath[i] != '/' {
- i--
- }
- if i < 0 {
- return "//" + fullPath[:i+1], fullPath[i+1:]
- }
- // do not include the / at the split
- return "//" + fullPath[:i], fullPath[i+1:]
- }
- // splitPath is modified splitPath version that doesn't include the separator
- // in the base path
- func (f *Fs) splitPath(pth string) (string, string) {
- // chop of any leading or trailing '/'
- pth = strings.Trim(pth, "/")
- i := len(pth) - 1
- for i >= 0 && pth[i] != '/' {
- i--
- }
- if i < 0 {
- return pth[:i+1], pth[i+1:]
- }
- return pth[:i], pth[i+1:]
- }
- // NewFs makes a new Fs object from the path
- //
- // The path is of the form remote:path
- //
- // Remotes are looked up in the config file. If the remote isn't
- // found then NotFoundInConfigFile will be returned.
- //
- // On Windows avoid single character remote names as they can be mixed
- // up with drive letters.
- func NewFs(ctx context.Context, name string, root string, config configmap.Mapper) (fs.Fs, error) {
- opt := new(Options)
- err := configstruct.Set(config, opt)
- if err != nil {
- return nil, err
- }
- f := &Fs{
- name: name,
- root: root,
- opt: *opt,
- pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant), pacer.AttackConstant(attackConstant))),
- }
- if root == "/" || root == "." {
- f.root = ""
- } else {
- f.root = root
- }
- f.features = (&fs.Features{
- DuplicateFiles: true,
- CanHaveEmptyDirectories: true,
- ReadMimeType: false,
- }).Fill(ctx, f)
- if f.opt.Private {
- f.public = "0"
- }
- client := fshttp.NewClient(ctx)
- f.srv = rest.NewClient(client).SetRoot(apiBaseURL)
- f.IDRegexp = regexp.MustCompile(`^https://uptobox\.com/([a-zA-Z0-9]+)`)
- _, err = f.readMetaDataForPath(ctx, f.dirPath(""), &api.MetadataRequestOptions{Limit: 10})
- if err != nil {
- if _, ok := err.(api.Error); !ok {
- return nil, err
- }
- // assume it's a file than
- oldRoot := f.root
- rootDir, file := f.splitPath(root)
- f.root = rootDir
- _, err = f.NewObject(ctx, file)
- if err == nil {
- return f, fs.ErrorIsFile
- }
- f.root = oldRoot
- }
- return f, nil
- }
- func (f *Fs) decodeError(resp *http.Response, response interface{}) (err error) {
- defer fs.CheckClose(resp.Body, &err)
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return err
- }
- // try to unmarshal into correct structure
- err = json.Unmarshal(body, response)
- if err == nil {
- return nil
- }
- // try to unmarshal into Error
- var apiErr api.Error
- err = json.Unmarshal(body, &apiErr)
- if err != nil {
- return err
- }
- return apiErr
- }
- func (f *Fs) readMetaDataForPath(ctx context.Context, path string, options *api.MetadataRequestOptions) (*api.ReadMetadataResponse, error) {
- opts := rest.Opts{
- Method: "GET",
- Path: "/user/files",
- Parameters: url.Values{
- "token": []string{f.opt.AccessToken},
- "path": []string{f.opt.Enc.FromStandardPath(path)},
- "limit": []string{strconv.FormatUint(options.Limit, 10)},
- },
- }
- if options.Offset != 0 {
- opts.Parameters.Set("offset", strconv.FormatUint(options.Offset, 10))
- }
- var err error
- var info api.ReadMetadataResponse
- var resp *http.Response
- err = f.pacer.Call(func() (bool, error) {
- resp, err = f.srv.Call(ctx, &opts)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return nil, err
- }
- err = f.decodeError(resp, &info)
- if err != nil {
- return nil, err
- }
- if info.StatusCode != 0 {
- return nil, errors.New(info.Message)
- }
- return &info, nil
- }
- // List the objects and directories in dir into entries. The
- // entries can be returned in any order but should be for a
- // complete directory.
- //
- // dir should be "" to list the root, and should not have
- // trailing slashes.
- //
- // This should return ErrDirNotFound if the directory isn't
- // found.
- func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
- root := f.dirPath(dir)
- var limit uint64 = 100 // max number of objects per request - 100 seems to be the maximum the api accepts
- var page uint64 = 1
- var offset uint64 // for the next page of requests
- for {
- opts := &api.MetadataRequestOptions{
- Limit: limit,
- Offset: offset,
- }
- info, err := f.readMetaDataForPath(ctx, root, opts)
- if err != nil {
- if apiErr, ok := err.(api.Error); ok {
- // might indicate other errors but we can probably assume not found here
- if apiErr.StatusCode == 1 {
- return nil, fs.ErrorDirNotFound
- }
- }
- return nil, err
- }
- for _, item := range info.Data.Files {
- remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name))
- o, err := f.newObjectWithInfo(ctx, remote, &item)
- if err != nil {
- continue
- }
- entries = append(entries, o)
- }
- // folders are always listed entirely on every page grr.
- if page == 1 {
- for _, item := range info.Data.Folders {
- remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name))
- d := fs.NewDir(remote, time.Time{}).SetID(strconv.FormatUint(item.FolderID, 10))
- entries = append(entries, d)
- }
- }
- //offset for the next page of items
- page++
- offset += limit
- //check if we reached end of list
- if page > uint64(info.Data.PageCount) {
- break
- }
- }
- return entries, nil
- }
- // Return an Object from a path
- //
- // If it can't be found it returns the error fs.ErrorObjectNotFound.
- func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.FileInfo) (fs.Object, error) {
- o := &Object{
- fs: f,
- remote: remote,
- size: info.Size,
- code: info.Code,
- hasMetaData: true,
- }
- return o, nil
- }
- // NewObject finds the Object at remote. If it can't be found it
- // returns the error fs.ErrorObjectNotFound.
- func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
- // no way to directly access an object by path so we have to list the parent dir
- entries, err := f.List(ctx, path.Dir(remote))
- if err != nil {
- // need to change error type
- // if the parent dir doesn't exist the object doesn't exist either
- if err == fs.ErrorDirNotFound {
- return nil, fs.ErrorObjectNotFound
- }
- return nil, err
- }
- for _, entry := range entries {
- if o, ok := entry.(fs.Object); ok {
- if o.Remote() == remote {
- return o, nil
- }
- }
- }
- return nil, fs.ErrorObjectNotFound
- }
- func (f *Fs) uploadFile(ctx context.Context, in io.Reader, size int64, filename string, uploadURL string, options ...fs.OpenOption) (*api.UploadResponse, error) {
- opts := rest.Opts{
- Method: "POST",
- RootURL: "https:" + uploadURL,
- Body: in,
- ContentLength: &size,
- Options: options,
- MultipartContentName: "files",
- MultipartFileName: filename,
- }
- var err error
- var resp *http.Response
- var ul api.UploadResponse
- err = f.pacer.CallNoRetry(func() (bool, error) {
- resp, err = f.srv.CallJSON(ctx, &opts, nil, &ul)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return nil, fmt.Errorf("couldn't upload file: %w", err)
- }
- return &ul, nil
- }
- // dstPath starts from root and includes //
- func (f *Fs) move(ctx context.Context, dstPath string, fileID string) (err error) {
- meta, err := f.readMetaDataForPath(ctx, dstPath, &api.MetadataRequestOptions{Limit: 10})
- if err != nil {
- return err
- }
- opts := rest.Opts{
- Method: "PATCH",
- Path: "/user/files",
- }
- mv := api.CopyMoveFileRequest{
- Token: f.opt.AccessToken,
- FileCodes: fileID,
- DestinationFolderID: meta.Data.CurrentFolder.FolderID,
- Action: "move",
- }
- var resp *http.Response
- var info api.UpdateResponse
- err = f.pacer.Call(func() (bool, error) {
- resp, err = f.srv.CallJSON(ctx, &opts, &mv, &info)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return fmt.Errorf("couldn't move file: %w", err)
- }
- if info.StatusCode != 0 {
- return fmt.Errorf("move: api error: %d - %s", info.StatusCode, info.Message)
- }
- return err
- }
- // updateFileInformation set's various file attributes most importantly it's name
- func (f *Fs) updateFileInformation(ctx context.Context, update *api.UpdateFileInformation) (err error) {
- opts := rest.Opts{
- Method: "PATCH",
- Path: "/user/files",
- }
- var resp *http.Response
- var info api.UpdateResponse
- err = f.pacer.Call(func() (bool, error) {
- resp, err = f.srv.CallJSON(ctx, &opts, update, &info)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return fmt.Errorf("couldn't update file info: %w", err)
- }
- if info.StatusCode != 0 {
- return fmt.Errorf("updateFileInfo: api error: %d - %s", info.StatusCode, info.Message)
- }
- return err
- }
- func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size int64, options ...fs.OpenOption) error {
- if size > int64(200e9) { // max size 200GB
- return errors.New("file too big, can't upload")
- } else if size == 0 {
- return fs.ErrorCantUploadEmptyFiles
- }
- // yes it does take 4 requests if we're uploading to root and 6+ if we're uploading to any subdir :(
- // create upload request
- opts := rest.Opts{
- Method: "GET",
- Path: "/upload",
- }
- token := api.Token{
- Token: f.opt.AccessToken,
- }
- var info api.UploadInfo
- err := f.pacer.Call(func() (bool, error) {
- resp, err := f.srv.CallJSON(ctx, &opts, &token, &info)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return err
- }
- if info.StatusCode != 0 {
- return fmt.Errorf("putUnchecked api error: %d - %s", info.StatusCode, info.Message)
- }
- // we need to have a safe name for the upload to work
- tmpName := "rcloneTemp" + random.String(8)
- upload, err := f.uploadFile(ctx, in, size, tmpName, info.Data.UploadLink, options...)
- if err != nil {
- return err
- }
- if len(upload.Files) != 1 {
- return errors.New("upload unexpected response")
- }
- match := f.IDRegexp.FindStringSubmatch(upload.Files[0].URL)
- // move file to destination folder
- base, leaf := f.splitPath(remote)
- fullBase := f.dirPath(base)
- if fullBase != "//" {
- // make all the parent folders
- err = f.Mkdir(ctx, base)
- if err != nil {
- // this might need some more error handling. if any of the following requests fail
- // we'll leave an orphaned temporary file floating around somewhere
- // they rarely fail though
- return err
- }
- err = f.move(ctx, fullBase, match[1])
- if err != nil {
- return err
- }
- }
- // rename file to final name
- err = f.updateFileInformation(ctx, &api.UpdateFileInformation{
- Token: f.opt.AccessToken,
- FileCode: match[1],
- NewName: f.opt.Enc.FromStandardName(leaf),
- Public: f.public,
- })
- if err != nil {
- return err
- }
- return nil
- }
- // Put in to the remote path with the modTime given of the given size
- //
- // When called from outside an Fs by rclone, src.Size() will always be >= 0.
- // But for unknown-sized objects (indicated by src.Size() == -1), Put should either
- // return an error or upload it properly (rather than e.g. calling panic).
- //
- // May create the object even if it returns an error - if so
- // will return the object and the error, otherwise will return
- // nil and the error
- func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
- existingObj, err := f.NewObject(ctx, src.Remote())
- switch err {
- case nil:
- return existingObj, existingObj.Update(ctx, in, src, options...)
- case fs.ErrorObjectNotFound:
- // Not found so create it
- return f.PutUnchecked(ctx, in, src, options...)
- default:
- return nil, err
- }
- }
- // PutUnchecked uploads the object
- //
- // This will create a duplicate if we upload a new file without
- // checking to see if there is one already - use Put() for that.
- func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
- err := f.putUnchecked(ctx, in, src.Remote(), src.Size(), options...)
- if err != nil {
- return nil, err
- }
- return f.NewObject(ctx, src.Remote())
- }
- // CreateDir dir creates a directory with the given parent path
- // base starts from root and may or may not include //
- func (f *Fs) CreateDir(ctx context.Context, base string, leaf string) (err error) {
- base = "//" + strings.Trim(base, "/")
- var resp *http.Response
- var apiErr api.Error
- opts := rest.Opts{
- Method: "PUT",
- Path: "/user/files",
- }
- mkdir := api.CreateFolderRequest{
- Name: f.opt.Enc.FromStandardName(leaf),
- Path: f.opt.Enc.FromStandardPath(base),
- Token: f.opt.AccessToken,
- }
- err = f.pacer.Call(func() (bool, error) {
- resp, err = f.srv.CallJSON(ctx, &opts, &mkdir, &apiErr)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return err
- }
- // checking if the dir exists beforehand would be slower so we'll just ignore the error here
- if apiErr.StatusCode != 0 && !strings.Contains(apiErr.Data, "already exists") {
- return apiErr
- }
- return nil
- }
- func (f *Fs) mkDirs(ctx context.Context, path string) (err error) {
- // chop of any leading or trailing slashes
- dirs := strings.Split(path, "/")
- var base = ""
- for _, element := range dirs {
- // create every dir one by one
- if element != "" {
- err = f.CreateDir(ctx, base, element)
- if err != nil {
- return err
- }
- base += "/" + element
- }
- }
- return nil
- }
- // Mkdir makes the directory (container, bucket)
- //
- // Shouldn't return an error if it already exists
- func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
- if dir == "" || dir == "." {
- return f.mkDirs(ctx, f.root)
- }
- return f.mkDirs(ctx, path.Join(f.root, dir))
- }
- // may or may not delete folders with contents?
- func (f *Fs) purge(ctx context.Context, folderID uint64) (err error) {
- var resp *http.Response
- var apiErr api.Error
- opts := rest.Opts{
- Method: "DELETE",
- Path: "/user/files",
- }
- rm := api.DeleteFolderRequest{
- FolderID: folderID,
- Token: f.opt.AccessToken,
- }
- err = f.pacer.Call(func() (bool, error) {
- resp, err = f.srv.CallJSON(ctx, &opts, &rm, &apiErr)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return err
- }
- if apiErr.StatusCode != 0 {
- return apiErr
- }
- return nil
- }
- // Rmdir removes the directory (container, bucket) if empty
- //
- // Return an error if it doesn't exist or isn't empty
- func (f *Fs) Rmdir(ctx context.Context, dir string) error {
- info, err := f.readMetaDataForPath(ctx, f.dirPath(dir), &api.MetadataRequestOptions{Limit: 10})
- if err != nil {
- return err
- }
- if len(info.Data.Folders) > 0 || len(info.Data.Files) > 0 {
- return fs.ErrorDirectoryNotEmpty
- }
- return f.purge(ctx, info.Data.CurrentFolder.FolderID)
- }
- // Move src to this remote using server side move operations.
- func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
- srcObj, ok := src.(*Object)
- if !ok {
- fs.Debugf(src, "Can't move - not same remote type")
- return nil, fs.ErrorCantMove
- }
- srcBase, srcLeaf := srcObj.fs.splitPathFull(src.Remote())
- dstBase, dstLeaf := f.splitPathFull(remote)
- needRename := srcLeaf != dstLeaf
- needMove := srcBase != dstBase
- // do the move if required
- if needMove {
- err := f.mkDirs(ctx, strings.Trim(dstBase, "/"))
- if err != nil {
- return nil, fmt.Errorf("move: failed to make destination dirs: %w", err)
- }
- err = f.move(ctx, dstBase, srcObj.code)
- if err != nil {
- return nil, err
- }
- }
- // rename to final name if we need to
- if needRename {
- err := f.updateFileInformation(ctx, &api.UpdateFileInformation{
- Token: f.opt.AccessToken,
- FileCode: srcObj.code,
- NewName: f.opt.Enc.FromStandardName(dstLeaf),
- Public: f.public,
- })
- if err != nil {
- return nil, fmt.Errorf("move: failed final rename: %w", err)
- }
- }
- // copy the old object and apply the changes
- newObj := *srcObj
- newObj.remote = remote
- newObj.fs = f
- return &newObj, nil
- }
- // renameDir renames a directory
- func (f *Fs) renameDir(ctx context.Context, folderID uint64, newName string) (err error) {
- var resp *http.Response
- var apiErr api.Error
- opts := rest.Opts{
- Method: "PATCH",
- Path: "/user/files",
- }
- rename := api.RenameFolderRequest{
- Token: f.opt.AccessToken,
- FolderID: folderID,
- NewName: newName,
- }
- err = f.pacer.Call(func() (bool, error) {
- resp, err = f.srv.CallJSON(ctx, &opts, &rename, &apiErr)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return err
- }
- if apiErr.StatusCode != 0 {
- return apiErr
- }
- return nil
- }
- // DirMove moves src, srcRemote to this remote at dstRemote
- // using server-side move operations.
- //
- // Will only be called if src.Fs().Name() == f.Name()
- //
- // If it isn't possible then return fs.ErrorCantDirMove
- //
- // If destination exists then return fs.ErrorDirExists
- func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
- srcFs, ok := src.(*Fs)
- if !ok {
- fs.Debugf(srcFs, "Can't move directory - not same remote type")
- return fs.ErrorCantDirMove
- }
- // find out source
- srcPath := srcFs.dirPath(srcRemote)
- srcInfo, err := f.readMetaDataForPath(ctx, srcPath, &api.MetadataRequestOptions{Limit: 1})
- if err != nil {
- return fmt.Errorf("dirmove: source not found: %w", err)
- }
- // check if the destination already exists
- dstPath := f.dirPath(dstRemote)
- _, err = f.readMetaDataForPath(ctx, dstPath, &api.MetadataRequestOptions{Limit: 1})
- if err == nil {
- return fs.ErrorDirExists
- }
- // make the destination parent path
- dstBase, dstName := f.splitPathFull(dstRemote)
- err = f.mkDirs(ctx, strings.Trim(dstBase, "/"))
- if err != nil {
- return fmt.Errorf("dirmove: failed to create dirs: %w", err)
- }
- // find the destination parent dir
- dstInfo, err := f.readMetaDataForPath(ctx, dstBase, &api.MetadataRequestOptions{Limit: 1})
- if err != nil {
- return fmt.Errorf("dirmove: failed to read destination: %w", err)
- }
- srcBase, srcName := srcFs.splitPathFull(srcRemote)
- needRename := srcName != dstName
- needMove := srcBase != dstBase
- // if we have to rename we'll have to use a temporary name since
- // there could already be a directory with the same name as the src directory
- if needRename {
- // rename to a temporary name
- tmpName := "rcloneTemp" + random.String(8)
- err = f.renameDir(ctx, srcInfo.Data.CurrentFolder.FolderID, tmpName)
- if err != nil {
- return fmt.Errorf("dirmove: failed initial rename: %w", err)
- }
- }
- // do the move
- if needMove {
- opts := rest.Opts{
- Method: "PATCH",
- Path: "/user/files",
- }
- move := api.MoveFolderRequest{
- Token: f.opt.AccessToken,
- FolderID: srcInfo.Data.CurrentFolder.FolderID,
- DestinationFolderID: dstInfo.Data.CurrentFolder.FolderID,
- Action: "move",
- }
- var resp *http.Response
- var apiErr api.Error
- err = f.pacer.Call(func() (bool, error) {
- resp, err = f.srv.CallJSON(ctx, &opts, &move, &apiErr)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return fmt.Errorf("dirmove: failed to move: %w", err)
- }
- if apiErr.StatusCode != 0 {
- return apiErr
- }
- }
- // rename to final name
- if needRename {
- err = f.renameDir(ctx, srcInfo.Data.CurrentFolder.FolderID, dstName)
- if err != nil {
- return fmt.Errorf("dirmove: failed final rename: %w", err)
- }
- }
- return nil
- }
- func (f *Fs) copy(ctx context.Context, dstPath string, fileID string) (err error) {
- meta, err := f.readMetaDataForPath(ctx, dstPath, &api.MetadataRequestOptions{Limit: 10})
- if err != nil {
- return err
- }
- opts := rest.Opts{
- Method: "PATCH",
- Path: "/user/files",
- }
- cp := api.CopyMoveFileRequest{
- Token: f.opt.AccessToken,
- FileCodes: fileID,
- DestinationFolderID: meta.Data.CurrentFolder.FolderID,
- Action: "copy",
- }
- var resp *http.Response
- var info api.UpdateResponse
- err = f.pacer.Call(func() (bool, error) {
- resp, err = f.srv.CallJSON(ctx, &opts, &cp, &info)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return fmt.Errorf("couldn't copy file: %w", err)
- }
- if info.StatusCode != 0 {
- return fmt.Errorf("copy: api error: %d - %s", info.StatusCode, info.Message)
- }
- return err
- }
- // Copy src to this remote using server side move operations.
- func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
- srcObj, ok := src.(*Object)
- if !ok {
- fs.Debugf(src, "Can't copy - not same remote type")
- return nil, fs.ErrorCantMove
- }
- _, srcLeaf := f.splitPath(src.Remote())
- dstBase, dstLeaf := f.splitPath(remote)
- needRename := srcLeaf != dstLeaf
- err := f.mkDirs(ctx, path.Join(f.root, dstBase))
- if err != nil {
- return nil, fmt.Errorf("copy: failed to make destination dirs: %w", err)
- }
- err = f.copy(ctx, f.dirPath(dstBase), srcObj.code)
- if err != nil {
- return nil, err
- }
- newObj, err := f.NewObject(ctx, path.Join(dstBase, srcLeaf))
- if err != nil {
- return nil, fmt.Errorf("copy: couldn't find copied object: %w", err)
- }
- if needRename {
- err := f.updateFileInformation(ctx, &api.UpdateFileInformation{
- Token: f.opt.AccessToken,
- FileCode: newObj.(*Object).code,
- NewName: f.opt.Enc.FromStandardName(dstLeaf),
- Public: f.public,
- })
- if err != nil {
- return nil, fmt.Errorf("copy: failed final rename: %w", err)
- }
- newObj.(*Object).remote = remote
- }
- return newObj, nil
- }
- // ------------------------------------------------------------
- // Fs returns the parent Fs
- func (o *Object) Fs() fs.Info {
- return o.fs
- }
- // Return a string version
- func (o *Object) String() string {
- if o == nil {
- return "<nil>"
- }
- return o.remote
- }
- // Remote returns the remote path
- func (o *Object) Remote() string {
- return o.remote
- }
- // ModTime returns the modification time of the object
- //
- // It attempts to read the objects mtime and if that isn't present the
- // LastModified returned in the http headers
- func (o *Object) ModTime(ctx context.Context) time.Time {
- ci := fs.GetConfig(ctx)
- return time.Time(ci.DefaultTime)
- }
- // Size returns the size of an object in bytes
- func (o *Object) Size() int64 {
- return o.size
- }
- // Hash returns the Md5sum of an object returning a lowercase hex string
- func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
- return "", hash.ErrUnsupported
- }
- // ID returns the ID of the Object if known, or "" if not
- func (o *Object) ID() string {
- return o.code
- }
- // Storable returns whether this object is storable
- func (o *Object) Storable() bool {
- return true
- }
- // SetModTime sets the modification time of the local fs object
- func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
- return fs.ErrorCantSetModTime
- }
- // Open an object for read
- func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
- opts := rest.Opts{
- Method: "GET",
- Path: "/link",
- Parameters: url.Values{
- "token": []string{o.fs.opt.AccessToken},
- "file_code": []string{o.code},
- },
- }
- var dl api.Download
- var resp *http.Response
- err = o.fs.pacer.Call(func() (bool, error) {
- resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &dl)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return nil, fmt.Errorf("open: failed to get download link: %w", err)
- }
- fs.FixRangeOption(options, o.size)
- opts = rest.Opts{
- Method: "GET",
- RootURL: dl.Data.DownloadLink,
- Options: options,
- }
- err = o.fs.pacer.Call(func() (bool, error) {
- resp, err = o.fs.srv.Call(ctx, &opts)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return nil, err
- }
- return resp.Body, err
- }
- // Update the already existing object
- //
- // Copy the reader into the object updating modTime and size.
- //
- // The new object may have been created if an error is returned
- func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
- if src.Size() < 0 {
- return errors.New("refusing to update with unknown size")
- }
- // upload with new size but old name
- err := o.fs.putUnchecked(ctx, in, o.Remote(), src.Size(), options...)
- if err != nil {
- return err
- }
- // delete duplicate object after successful upload
- err = o.Remove(ctx)
- if err != nil {
- return fmt.Errorf("failed to remove old version: %w", err)
- }
- // Fetch new object after deleting the duplicate
- info, err := o.fs.NewObject(ctx, o.Remote())
- if err != nil {
- return err
- }
- // Replace guts of old object with new one
- *o = *info.(*Object)
- return nil
- }
- // Remove an object
- func (o *Object) Remove(ctx context.Context) error {
- opts := rest.Opts{
- Method: "DELETE",
- Path: "/user/files",
- }
- delete := api.RemoveFileRequest{
- Token: o.fs.opt.AccessToken,
- FileCodes: o.code,
- }
- var info api.UpdateResponse
- err := o.fs.pacer.Call(func() (bool, error) {
- resp, err := o.fs.srv.CallJSON(ctx, &opts, &delete, &info)
- return shouldRetry(ctx, resp, err)
- })
- if err != nil {
- return err
- }
- if info.StatusCode != 0 {
- return fmt.Errorf("remove: api error: %d - %s", info.StatusCode, info.Message)
- }
- return nil
- }
- // Check the interfaces are satisfied
- var (
- _ fs.Fs = (*Fs)(nil)
- _ fs.Copier = (*Fs)(nil)
- _ fs.Mover = (*Fs)(nil)
- _ fs.DirMover = (*Fs)(nil)
- _ fs.Object = (*Object)(nil)
- )
|