jottacloud: add support for upload to custom device and mountpoint
See #5926
This commit is contained in:
parent
700ca23a71
commit
01340acad2
2 changed files with 210 additions and 52 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue