diff --git a/backend/jottacloud/jottacloud.go b/backend/jottacloud/jottacloud.go index 3322d8d8c..65df99f58 100644 --- a/backend/jottacloud/jottacloud.go +++ b/backend/jottacloud/jottacloud.go @@ -127,7 +127,7 @@ func init() { func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) { switch config.State { case "": - return fs.ConfigChooseExclusiveFixed("auth_type_done", "config_type", `Authentication type.`, []fs.OptionExample{{ + return fs.ConfigChooseExclusiveFixed("auth_type_done", "config_type", `Select authentication type.`, []fs.OptionExample{{ Value: "standard", Help: "Standard authentication.\nUse this if you're a normal Jottacloud user.", }, { @@ -145,7 +145,7 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf return fs.ConfigGoto(config.Result) case "standard": // configure a jottacloud backend using the modern JottaCli token based authentication m.Set("configVersion", fmt.Sprint(configVersion)) - return fs.ConfigInput("standard_token", "config_login_token", "Personal login token.\n\nGenerate here: https://www.jottacloud.com/web/secure") + return fs.ConfigInput("standard_token", "config_login_token", "Personal login token.\nGenerate here: https://www.jottacloud.com/web/secure") case "standard_token": loginToken := config.Result m.Set(configClientID, defaultClientID) @@ -262,7 +262,11 @@ machines.`) }, }) case "choose_device": - return fs.ConfigConfirm("choose_device_query", false, "config_non_standard", "Use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?") + return fs.ConfigConfirm("choose_device_query", false, "config_non_standard", `Use a non-standard device/mountpoint? +Choosing no, the default, will let you access the storage used for the archive +section of the official Jottacloud client. If you instead want to access the +sync or the backup section, for example, you must choose yes.`) + case "choose_device_query": if config.Result != "true" { m.Set(configDevice, "") @@ -286,12 +290,27 @@ machines.`) if err != nil { return nil, err } - return fs.ConfigChooseExclusive("choose_device_result", "config_device", `Please select the device to use. Normally this will be Jotta`, len(acc.Devices), func(i int) (string, string) { - return acc.Devices[i].Name, "" + + deviceNames := make([]string, len(acc.Devices)) + for i, dev := range acc.Devices { + if i > 0 && dev.Name == defaultDevice { + // Insert the special Jotta device as first entry, making it the default choice. + copy(deviceNames[1:i+1], deviceNames[0:i]) + deviceNames[0] = dev.Name + } else { + deviceNames[i] = dev.Name + } + } + + help := fmt.Sprintf(`The device to use. In standard setup the built-in %s device is used, +which contains predefined mountpoints for archive, sync etc. All other devices +are treated as backup devices by the official Jottacloud client. You may create +a new by entering a unique name.`, defaultDevice) + return fs.ConfigChoose("choose_device_result", "config_device", help, len(deviceNames), func(i int) (string, string) { + return deviceNames[i], "" }) case "choose_device_result": device := config.Result - m.Set(configDevice, device) oAuthClient, _, err := getOAuthClient(ctx, name, m) if err != nil { @@ -300,16 +319,89 @@ machines.`) srv := rest.NewClient(oAuthClient).SetRoot(rootURL) username, _ := m.Get(configUsername) - dev, err := getDeviceInfo(ctx, srv, path.Join(username, device)) + + acc, err := getDriveInfo(ctx, srv, username) if err != nil { return nil, err } - return fs.ConfigChooseExclusive("choose_device_mountpoint", "config_mountpoint", `Please select the mountpoint to use. Normally this will be Archive.`, len(dev.MountPoints), func(i int) (string, string) { + isNew := true + for _, dev := range acc.Devices { + if strings.EqualFold(dev.Name, device) { // If device name exists with different casing we prefer the existing (not sure if and how the api handles the opposite) + device = dev.Name // Prefer same casing as existing, e.g. if user entered "jotta" we use the standard casing "Jotta" instead + isNew = false + break + } + } + var dev *api.JottaDevice + if isNew { + fs.Debugf(nil, "Creating new device: %s", device) + dev, err = createDevice(ctx, srv, path.Join(username, device)) + if err != nil { + return nil, err + } + } + m.Set(configDevice, device) + + if !isNew { + dev, err = getDeviceInfo(ctx, srv, path.Join(username, device)) + if err != nil { + return nil, err + } + } + + var help string + if device == defaultDevice { + // With built-in Jotta device the mountpoint choice is exclusive, + // we do not want to risk any problems by creating new mountpoints on it. + help = fmt.Sprintf(`The mountpoint to use on the built-in device %s. +The standard setup is to use the %s mountpoint. Most other mountpoints +have very limited support in rclone and should generally be avoided.`, defaultDevice, defaultMountpoint) + return fs.ConfigChooseExclusive("choose_device_mountpoint", "config_mountpoint", help, len(dev.MountPoints), func(i int) (string, string) { + return dev.MountPoints[i].Name, "" + }) + } + help = fmt.Sprintf(`The mountpoint to use on the non-standard device %s. +You may create a new by entering a unique name.`, device) + return fs.ConfigChoose("choose_device_mountpoint", "config_mountpoint", help, len(dev.MountPoints), func(i int) (string, string) { return dev.MountPoints[i].Name, "" }) case "choose_device_mountpoint": mountpoint := config.Result + + oAuthClient, _, err := getOAuthClient(ctx, name, m) + if err != nil { + return nil, err + } + srv := rest.NewClient(oAuthClient).SetRoot(rootURL) + + username, _ := m.Get(configUsername) + device, _ := m.Get(configDevice) + + dev, err := getDeviceInfo(ctx, srv, path.Join(username, device)) + if err != nil { + return nil, err + } + isNew := true + for _, mnt := range dev.MountPoints { + if strings.EqualFold(mnt.Name, mountpoint) { + mountpoint = mnt.Name + isNew = false + break + } + } + + if isNew { + if device == defaultDevice { + return nil, fmt.Errorf("custom mountpoints not supported on built-in %s device: %w", defaultDevice, err) + } + fs.Debugf(nil, "Creating new mountpoint: %s", mountpoint) + _, err := createMountPoint(ctx, srv, path.Join(username, device, mountpoint)) + if err != nil { + return nil, err + } + } m.Set(configMountpoint, mountpoint) + return fs.ConfigGoto("end") case "end": // All the config flows end up here in case we need to carry on with something @@ -338,6 +430,7 @@ type Fs struct { opt Options features *fs.Features endpointURL string + allocateURL string srv *rest.Client apiSrv *rest.Client pacer *fs.Pacer @@ -588,6 +681,37 @@ func getDeviceInfo(ctx context.Context, srv *rest.Client, path string) (info *ap return info, nil } +// createDevice makes a device +func createDevice(ctx context.Context, srv *rest.Client, path string) (info *api.JottaDevice, err error) { + opts := rest.Opts{ + Method: "POST", + Path: urlPathEscape(path), + Parameters: url.Values{}, + } + + opts.Parameters.Set("type", "WORKSTATION") + + _, err = srv.CallXML(ctx, &opts, nil, &info) + if err != nil { + return nil, fmt.Errorf("couldn't create device: %w", err) + } + return info, nil +} + +// createMountPoint makes a mount point +func createMountPoint(ctx context.Context, srv *rest.Client, path string) (info *api.JottaMountPoint, err error) { + opts := rest.Opts{ + Method: "POST", + Path: urlPathEscape(path), + } + + _, err = srv.CallXML(ctx, &opts, nil, &info) + if err != nil { + return nil, fmt.Errorf("couldn't create mountpoint: %w", err) + } + return info, nil +} + // setEndpointURL generates the API endpoint URL func (f *Fs) setEndpointURL() { if f.opt.Device == "" { @@ -597,6 +721,7 @@ func (f *Fs) setEndpointURL() { f.opt.Mountpoint = defaultMountpoint } f.endpointURL = path.Join(f.user, f.opt.Device, f.opt.Mountpoint) + f.allocateURL = path.Join("/jfs", f.opt.Device, f.opt.Mountpoint) } // readMetaDataForPath reads the metadata from the path @@ -662,6 +787,11 @@ func (f *Fs) filePath(file string) string { return urlPathEscape(f.filePathRaw(file)) } +// allocatePathRaw returns an unescaped file path (f.root, file) +func (f *Fs) allocatePathRaw(file string) string { + return path.Join(f.endpointURL, f.opt.Enc.FromStandardPath(path.Join(f.root, file))) +} + // Jottacloud requires the grant_type 'refresh_token' string // to be uppercase and throws a 400 Bad Request if we use the // lower case used by the oauth2 module @@ -1101,9 +1231,6 @@ func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Obje // // The new object may have been created if an error is returned func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { - if f.opt.Device != "Jotta" { - return nil, errors.New("upload not supported for devices other than Jotta") - } o := f.createObject(src.Remote(), src.ModTime(ctx), src.Size()) return o, o.Update(ctx, in, src, options...) } @@ -1738,7 +1865,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op Created: fileDate, Modified: fileDate, Md5: md5String, - Path: path.Join(o.fs.opt.Mountpoint, o.fs.opt.Enc.FromStandardPath(path.Join(o.fs.root, o.remote))), + Path: path.Join(o.fs.allocateURL, o.fs.opt.Enc.FromStandardPath(path.Join(o.fs.root, o.remote))), } // send it diff --git a/docs/content/jottacloud.md b/docs/content/jottacloud.md index 77afba24a..ef7ea5586 100644 --- a/docs/content/jottacloud.md +++ b/docs/content/jottacloud.md @@ -75,60 +75,83 @@ s) Set configuration password q) Quit config n/s/q> n name> remote +Option Storage. Type of storage to configure. -Enter a string value. Press Enter for the default (""). -Choose a number from below, or type in your own value +Choose a number from below, or type in your own value. [snip] XX / Jottacloud - \ "jottacloud" + \ (jottacloud) [snip] Storage> jottacloud -** See help for jottacloud backend at: https://rclone.org/jottacloud/ ** - -Edit advanced config? (y/n) -y) Yes -n) No -y/n> n -Remote config -Use legacy authentication?. -This is only required for certain whitelabel versions of Jottacloud and not recommended for normal users. +Edit advanced config? y) Yes n) No (default) y/n> n - -Generate a personal login token here: https://www.jottacloud.com/web/secure +Option config_type. +Select authentication type. +Choose a number from below, or type in an existing string value. +Press Enter for the default (standard). + / Standard authentication. + 1 | Use this if you're a normal Jottacloud user. + \ (standard) + / Legacy authentication. + 2 | This is only required for certain whitelabel versions of Jottacloud and not recommended for normal users. + \ (legacy) + / Telia Cloud authentication. + 3 | Use this if you are using Telia Cloud. + \ (telia) + / Tele2 Cloud authentication. + 4 | Use this if you are using Tele2 Cloud. + \ (tele2) +config_type> 1 +Personal login token. +Generate here: https://www.jottacloud.com/web/secure Login Token> - -Do you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client? - +Use a non-standard device/mountpoint? +Choosing no, the default, will let you access the storage used for the archive +section of the official Jottacloud client. If you instead want to access the +sync or the backup section, for example, you must choose yes. y) Yes -n) No +n) No (default) y/n> y -Please select the device to use. Normally this will be Jotta -Choose a number from below, or type in an existing value +Option config_device. +The device to use. In standard setup the built-in Jotta device is used, +which contains predefined mountpoints for archive, sync etc. All other devices +are treated as backup devices by the official Jottacloud client. You may create +a new by entering a unique name. +Choose a number from below, or type in your own string value. +Press Enter for the default (DESKTOP-3H31129). 1 > DESKTOP-3H31129 2 > Jotta -Devices> 2 -Please select the mountpoint to user. Normally this will be Archive -Choose a number from below, or type in an existing value +config_device> 2 +Option config_mountpoint. +The mountpoint to use for the built-in device Jotta. +The standard setup is to use the Archive mountpoint. Most other mountpoints +have very limited support in rclone and should generally be avoided. +Choose a number from below, or type in an existing string value. +Press Enter for the default (Archive). 1 > Archive - 2 > Links + 2 > Shared 3 > Sync - -Mountpoints> 1 +config_mountpoint> 1 -------------------- -[jotta] +[remote] type = jottacloud +configVersion = 1 +client_id = jottacli +client_secret = +tokenURL = https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token token = {........} +username = 2940e57271a93d987d6f8a21 device = Jotta mountpoint = Archive -configVersion = 1 -------------------- -y) Yes this is OK +y) Yes this is OK (default) e) Edit this remote d) Delete this remote y/e/d> y ``` + Once configured you can then use `rclone` like this, List directories in top level of your Jottacloud @@ -145,19 +168,27 @@ To copy a local directory to an Jottacloud directory called backup ### Devices and Mountpoints -The official Jottacloud client registers a device for each computer you install it on, -and then creates a mountpoint for each folder you select for Backup. -The web interface uses a special device called Jotta for the Archive and Sync mountpoints. +The official Jottacloud client registers a device for each computer you install +it on, and shows them in the backup section of the user interface. For each +folder you select for backup it will create a mountpoint within this device. +A built-in device called Jotta is special, and contains mountpoints Archive, +Sync and some others, used for corresponding features in official clients. -With rclone you'll want to use the Jotta/Archive device/mountpoint in most cases, however if you -want to access files uploaded by any of the official clients rclone provides the option to select -other devices and mountpoints during config. Note that uploading files is currently not supported -to other devices than Jotta. +With rclone you'll want to use the standard Jotta/Archive device/mountpoint in +most cases. However, you may for example want to access files from the sync or +backup functionality provided by the official clients, and rclone therefore +provides the option to select other devices and mountpoints during config. -The built-in Jotta device may also contain several other mountpoints, such as: Latest, Links, Shared and Trash. -These are special mountpoints with a different internal representation than the "regular" mountpoints. -Rclone will only to a very limited degree support them. Generally you should avoid these, unless you know what you -are doing. +You are allowed to create new devices and mountpoints. All devices except the +built-in Jotta device are treated as backup devices by official Jottacloud +clients, and the mountpoints on them are individual backup sets. + +With the built-in Jotta device, only existing, built-in, mountpoints can be +selected. In addition to the mentioned Archive and Sync, it may contain +several other mountpoints such as: Latest, Links, Shared and Trash. All of +these are special mountpoints with a different internal representation than +the "regular" mountpoints. Rclone will only to a very limited degree support +them. Generally you should avoid these, unless you know what you are doing. ### --fast-list