jottacloud: add support for upload to custom device and mountpoint

See #5926
This commit is contained in:
albertony 2022-01-18 20:59:50 +01:00
parent 700ca23a71
commit 01340acad2
2 changed files with 210 additions and 52 deletions

View file

@ -127,7 +127,7 @@ func init() {
func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) { func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
switch config.State { switch config.State {
case "": 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", Value: "standard",
Help: "Standard authentication.\nUse this if you're a normal Jottacloud user.", 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) return fs.ConfigGoto(config.Result)
case "standard": // configure a jottacloud backend using the modern JottaCli token based authentication case "standard": // configure a jottacloud backend using the modern JottaCli token based authentication
m.Set("configVersion", fmt.Sprint(configVersion)) 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": case "standard_token":
loginToken := config.Result loginToken := config.Result
m.Set(configClientID, defaultClientID) m.Set(configClientID, defaultClientID)
@ -262,7 +262,11 @@ machines.`)
}, },
}) })
case "choose_device": 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": case "choose_device_query":
if config.Result != "true" { if config.Result != "true" {
m.Set(configDevice, "") m.Set(configDevice, "")
@ -286,12 +290,27 @@ machines.`)
if err != nil { if err != nil {
return nil, err 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": case "choose_device_result":
device := config.Result device := config.Result
m.Set(configDevice, device)
oAuthClient, _, err := getOAuthClient(ctx, name, m) oAuthClient, _, err := getOAuthClient(ctx, name, m)
if err != nil { if err != nil {
@ -300,16 +319,89 @@ machines.`)
srv := rest.NewClient(oAuthClient).SetRoot(rootURL) srv := rest.NewClient(oAuthClient).SetRoot(rootURL)
username, _ := m.Get(configUsername) username, _ := m.Get(configUsername)
dev, err := getDeviceInfo(ctx, srv, path.Join(username, device))
acc, err := getDriveInfo(ctx, srv, username)
if err != nil { if err != nil {
return nil, err 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, "" return dev.MountPoints[i].Name, ""
}) })
case "choose_device_mountpoint": case "choose_device_mountpoint":
mountpoint := config.Result 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) m.Set(configMountpoint, mountpoint)
return fs.ConfigGoto("end") return fs.ConfigGoto("end")
case "end": case "end":
// All the config flows end up here in case we need to carry on with something // 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 opt Options
features *fs.Features features *fs.Features
endpointURL string endpointURL string
allocateURL string
srv *rest.Client srv *rest.Client
apiSrv *rest.Client apiSrv *rest.Client
pacer *fs.Pacer pacer *fs.Pacer
@ -588,6 +681,37 @@ func getDeviceInfo(ctx context.Context, srv *rest.Client, path string) (info *ap
return info, nil 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 // setEndpointURL generates the API endpoint URL
func (f *Fs) setEndpointURL() { func (f *Fs) setEndpointURL() {
if f.opt.Device == "" { if f.opt.Device == "" {
@ -597,6 +721,7 @@ func (f *Fs) setEndpointURL() {
f.opt.Mountpoint = defaultMountpoint f.opt.Mountpoint = defaultMountpoint
} }
f.endpointURL = path.Join(f.user, f.opt.Device, f.opt.Mountpoint) 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 // readMetaDataForPath reads the metadata from the path
@ -662,6 +787,11 @@ func (f *Fs) filePath(file string) string {
return urlPathEscape(f.filePathRaw(file)) 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 // Jottacloud requires the grant_type 'refresh_token' string
// to be uppercase and throws a 400 Bad Request if we use the // to be uppercase and throws a 400 Bad Request if we use the
// lower case used by the oauth2 module // 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 // 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) { 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()) o := f.createObject(src.Remote(), src.ModTime(ctx), src.Size())
return o, o.Update(ctx, in, src, options...) 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, Created: fileDate,
Modified: fileDate, Modified: fileDate,
Md5: md5String, 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 // send it

View file

@ -75,60 +75,83 @@ s) Set configuration password
q) Quit config q) Quit config
n/s/q> n n/s/q> n
name> remote name> remote
Option Storage.
Type of storage to configure. 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] [snip]
XX / Jottacloud XX / Jottacloud
\ "jottacloud" \ (jottacloud)
[snip] [snip]
Storage> jottacloud Storage> jottacloud
** See help for jottacloud backend at: https://rclone.org/jottacloud/ ** Edit advanced config?
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.
y) Yes y) Yes
n) No (default) n) No (default)
y/n> n y/n> n
Option config_type.
Generate a personal login token here: https://www.jottacloud.com/web/secure 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> <your token here> Login Token> <your token here>
Use a non-standard device/mountpoint?
Do you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client? 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 y) Yes
n) No n) No (default)
y/n> y y/n> y
Please select the device to use. Normally this will be Jotta Option config_device.
Choose a number from below, or type in an existing value 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 1 > DESKTOP-3H31129
2 > Jotta 2 > Jotta
Devices> 2 config_device> 2
Please select the mountpoint to user. Normally this will be Archive Option config_mountpoint.
Choose a number from below, or type in an existing value 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 1 > Archive
2 > Links 2 > Shared
3 > Sync 3 > Sync
config_mountpoint> 1
Mountpoints> 1
-------------------- --------------------
[jotta] [remote]
type = jottacloud type = jottacloud
configVersion = 1
client_id = jottacli
client_secret =
tokenURL = https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token
token = {........} token = {........}
username = 2940e57271a93d987d6f8a21
device = Jotta device = Jotta
mountpoint = Archive mountpoint = Archive
configVersion = 1
-------------------- --------------------
y) Yes this is OK y) Yes this is OK (default)
e) Edit this remote e) Edit this remote
d) Delete this remote d) Delete this remote
y/e/d> y y/e/d> y
``` ```
Once configured you can then use `rclone` like this, Once configured you can then use `rclone` like this,
List directories in top level of your Jottacloud 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 ### Devices and Mountpoints
The official Jottacloud client registers a device for each computer you install it on, The official Jottacloud client registers a device for each computer you install
and then creates a mountpoint for each folder you select for Backup. it on, and shows them in the backup section of the user interface. For each
The web interface uses a special device called Jotta for the Archive and Sync mountpoints. 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 With rclone you'll want to use the standard Jotta/Archive device/mountpoint in
want to access files uploaded by any of the official clients rclone provides the option to select most cases. However, you may for example want to access files from the sync or
other devices and mountpoints during config. Note that uploading files is currently not supported backup functionality provided by the official clients, and rclone therefore
to other devices than Jotta. 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. You are allowed to create new devices and mountpoints. All devices except the
These are special mountpoints with a different internal representation than the "regular" mountpoints. built-in Jotta device are treated as backup devices by official Jottacloud
Rclone will only to a very limited degree support them. Generally you should avoid these, unless you know what you clients, and the mountpoints on them are individual backup sets.
are doing.
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 ### --fast-list