2022-08-28 11:21:57 +00:00
// Package jottacloud provides an interface to the Jottacloud storage system.
2018-08-01 23:02:35 +00:00
package jottacloud
import (
2018-08-14 21:15:21 +00:00
"bytes"
2019-06-17 08:34:30 +00:00
"context"
2018-08-14 21:15:21 +00:00
"crypto/md5"
2019-11-19 23:10:38 +00:00
"encoding/base64"
2018-08-14 21:15:21 +00:00
"encoding/hex"
2019-11-19 23:10:38 +00:00
"encoding/json"
2021-11-30 09:21:05 +00:00
"encoding/xml"
2021-11-04 10:12:57 +00:00
"errors"
2018-08-01 23:02:35 +00:00
"fmt"
"io"
2020-06-11 11:02:28 +00:00
"math/rand"
2018-08-01 23:02:35 +00:00
"net/http"
"net/url"
2018-08-14 21:15:21 +00:00
"os"
2018-08-01 23:02:35 +00:00
"path"
"strconv"
"strings"
"time"
2019-07-28 17:47:38 +00:00
"github.com/rclone/rclone/backend/jottacloud/api"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/config/configstruct"
2020-06-11 11:02:28 +00:00
"github.com/rclone/rclone/fs/config/obscure"
2019-07-28 17:47:38 +00:00
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/fshttp"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/walk"
2020-01-14 17:33:35 +00:00
"github.com/rclone/rclone/lib/encoder"
2019-07-28 17:47:38 +00:00
"github.com/rclone/rclone/lib/oauthutil"
"github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/rest"
2018-08-22 20:26:18 +00:00
"golang.org/x/oauth2"
2018-08-01 23:02:35 +00:00
)
// Globals
const (
2020-06-11 11:02:28 +00:00
minSleep = 10 * time . Millisecond
maxSleep = 2 * time . Second
decayConstant = 2 // bigger for slower decay, exponential
defaultDevice = "Jotta"
defaultMountpoint = "Archive"
2022-01-18 20:06:22 +00:00
jfsURL = "https://jfs.jottacloud.com/jfs/"
2020-06-11 11:02:28 +00:00
apiURL = "https://api.jottacloud.com/"
2022-01-18 20:06:22 +00:00
wwwURL = "https://www.jottacloud.com/"
2020-06-11 11:02:28 +00:00
cachePrefix = "rclone-jcmd5-"
configDevice = "device"
configMountpoint = "mountpoint"
configTokenURL = "tokenURL"
configClientID = "client_id"
configClientSecret = "client_secret"
2021-04-29 08:28:18 +00:00
configUsername = "username"
2020-06-11 11:02:28 +00:00
configVersion = 1
2021-05-04 13:13:12 +00:00
defaultTokenURL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token"
defaultClientID = "jottacli"
legacyTokenURL = "https://api.jottacloud.com/auth/v1/token"
legacyRegisterURL = "https://api.jottacloud.com/auth/v1/register"
legacyClientID = "nibfk8biu12ju7hpqomr8b1e40"
legacyEncryptedClientSecret = "Vp8eAv7eVElMnQwN-kgU9cbhgApNDaMqWdlDi5qFydlQoji4JBxrGMF2"
legacyConfigVersion = 0
2021-01-17 01:38:57 +00:00
2023-09-01 13:55:32 +00:00
teliaseCloudTokenURL = "https://cloud-auth.telia.se/auth/realms/telia_se/protocol/openid-connect/token"
teliaseCloudAuthURL = "https://cloud-auth.telia.se/auth/realms/telia_se/protocol/openid-connect/auth"
teliaseCloudClientID = "desktop"
telianoCloudTokenURL = "https://sky-auth.telia.no/auth/realms/get/protocol/openid-connect/token"
telianoCloudAuthURL = "https://sky-auth.telia.no/auth/realms/get/protocol/openid-connect/auth"
telianoCloudClientID = "desktop"
2021-11-10 07:52:44 +00:00
tele2CloudTokenURL = "https://mittcloud-auth.tele2.se/auth/realms/comhem/protocol/openid-connect/token"
tele2CloudAuthURL = "https://mittcloud-auth.tele2.se/auth/realms/comhem/protocol/openid-connect/auth"
tele2CloudClientID = "desktop"
2023-07-02 10:16:07 +00:00
onlimeCloudTokenURL = "https://cloud-auth.onlime.dk/auth/realms/onlime_wl/protocol/openid-connect/token"
onlimeCloudAuthURL = "https://cloud-auth.onlime.dk/auth/realms/onlime_wl/protocol/openid-connect/auth"
onlimeCloudClientID = "desktop"
2018-08-22 20:26:18 +00:00
)
2018-08-01 23:02:35 +00:00
// Register with Fs
func init ( ) {
2019-05-17 00:31:59 +00:00
// needs to be done early so we can use oauth during config
2018-08-01 23:02:35 +00:00
fs . Register ( & fs . RegInfo {
Name : "jottacloud" ,
2020-05-20 10:54:33 +00:00
Description : "Jottacloud" ,
2018-08-01 23:02:35 +00:00
NewFs : NewFs ,
2021-04-29 08:28:18 +00:00
Config : Config ,
2022-08-04 18:48:13 +00:00
MetadataInfo : & fs . MetadataInfo {
Help : ` Jottacloud has limited support for metadata, currently an extended set of timestamps. ` ,
System : map [ string ] fs . MetadataHelp {
"btime" : {
Help : "Time of file birth (creation), read from rclone metadata" ,
Type : "RFC 3339" ,
Example : "2006-01-02T15:04:05.999999999Z07:00" ,
} ,
"mtime" : {
Help : "Time of last modification, read from rclone metadata" ,
Type : "RFC 3339" ,
Example : "2006-01-02T15:04:05.999999999Z07:00" ,
} ,
"utime" : {
Help : "Time of last upload, when current revision was created, generated by backend" ,
Type : "RFC 3339" ,
Example : "2006-01-02T15:04:05.999999999Z07:00" ,
ReadOnly : true ,
} ,
"content-type" : {
Help : "MIME type, also known as media type" ,
Type : "string" ,
Example : "text/plain" ,
ReadOnly : true ,
} ,
} ,
} ,
2023-07-06 16:55:53 +00:00
Options : append ( oauthutil . SharedOptions , [ ] fs . Option { {
2018-08-14 21:15:21 +00:00
Name : "md5_memory_limit" ,
2018-08-20 14:38:21 +00:00
Help : "Files bigger than this will be cached on disk to calculate the MD5 if required." ,
2018-08-14 21:15:21 +00:00
Default : fs . SizeSuffix ( 10 * 1024 * 1024 ) ,
Advanced : true ,
2020-04-11 10:11:20 +00:00
} , {
Name : "trashed_only" ,
2021-08-16 09:30:01 +00:00
Help : "Only show files that are in the trash.\n\nThis will show trashed files in their original directory structure." ,
2020-04-11 10:11:20 +00:00
Default : false ,
Advanced : true ,
2018-09-07 11:58:18 +00:00
} , {
Name : "hard_delete" ,
Help : "Delete files permanently rather than putting them into the trash." ,
Default : false ,
Advanced : true ,
2018-08-22 20:26:18 +00:00
} , {
Name : "upload_resume_limit" ,
2019-02-07 17:41:17 +00:00
Help : "Files bigger than this can be resumed if the upload fail's." ,
2018-08-22 20:26:18 +00:00
Default : fs . SizeSuffix ( 10 * 1024 * 1024 ) ,
Advanced : true ,
2021-07-01 21:17:05 +00:00
} , {
Name : "no_versions" ,
Help : "Avoid server side versioning by deleting files and recreating files instead of overwriting them." ,
Default : false ,
Advanced : true ,
2020-01-14 17:33:35 +00:00
} , {
Name : config . ConfigEncoding ,
Help : config . ConfigEncodingHelp ,
Advanced : true ,
2020-01-14 21:51:49 +00:00
// Encode invalid UTF-8 bytes as xml doesn't handle them properly.
//
// Also: '*', '/', ':', '<', '>', '?', '\"', '\x00', '|'
Default : ( encoder . Display |
encoder . EncodeWin | // :?"*<>|
encoder . EncodeInvalidUtf8 ) ,
2023-07-06 16:55:53 +00:00
} } ... ) ,
2018-08-01 23:02:35 +00:00
} )
}
2021-04-29 08:28:18 +00:00
// Config runs the backend configuration protocol
func Config ( ctx context . Context , name string , m configmap . Mapper , config fs . ConfigIn ) ( * fs . ConfigOut , error ) {
switch config . State {
case "" :
2022-01-18 19:59:50 +00:00
return fs . ConfigChooseExclusiveFixed ( "auth_type_done" , "config_type" , ` Select authentication type. ` , [ ] fs . OptionExample { {
2021-04-29 08:28:18 +00:00
Value : "standard" ,
2021-08-16 09:30:01 +00:00
Help : "Standard authentication.\nUse this if you're a normal Jottacloud user." ,
2021-04-29 08:28:18 +00:00
} , {
Value : "legacy" ,
2021-08-16 09:30:01 +00:00
Help : "Legacy authentication.\nThis is only required for certain whitelabel versions of Jottacloud and not recommended for normal users." ,
2021-04-29 08:28:18 +00:00
} , {
2023-09-01 13:55:32 +00:00
Value : "telia_se" ,
Help : "Telia Cloud authentication.\nUse this if you are using Telia Cloud (Sweden)." ,
} , {
Value : "telia_no" ,
Help : "Telia Sky authentication.\nUse this if you are using Telia Sky (Norway)." ,
2021-11-10 07:52:44 +00:00
} , {
Value : "tele2" ,
Help : "Tele2 Cloud authentication.\nUse this if you are using Tele2 Cloud." ,
2023-07-02 10:16:07 +00:00
} , {
Value : "onlime" ,
Help : "Onlime Cloud authentication.\nUse this if you are using Onlime Cloud." ,
2021-04-29 08:28:18 +00:00
} } )
case "auth_type_done" :
// Jump to next state according to config chosen
return fs . ConfigGoto ( config . Result )
case "standard" : // configure a jottacloud backend using the modern JottaCli token based authentication
m . Set ( "configVersion" , fmt . Sprint ( configVersion ) )
2022-01-18 19:59:50 +00:00
return fs . ConfigInput ( "standard_token" , "config_login_token" , "Personal login token.\nGenerate here: https://www.jottacloud.com/web/secure" )
2021-04-29 08:28:18 +00:00
case "standard_token" :
loginToken := config . Result
2021-05-04 13:13:12 +00:00
m . Set ( configClientID , defaultClientID )
2021-04-29 08:28:18 +00:00
m . Set ( configClientSecret , "" )
srv := rest . NewClient ( fshttp . NewClient ( ctx ) )
2022-07-12 20:10:04 +00:00
token , tokenEndpoint , err := doTokenAuth ( ctx , srv , loginToken )
2021-04-29 08:28:18 +00:00
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "failed to get oauth token: %w" , err )
2021-04-29 08:28:18 +00:00
}
2021-05-04 13:13:12 +00:00
m . Set ( configTokenURL , tokenEndpoint )
2021-04-29 08:28:18 +00:00
err = oauthutil . PutToken ( name , m , & token , true )
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "error while saving token: %w" , err )
2021-04-29 08:28:18 +00:00
}
return fs . ConfigGoto ( "choose_device" )
case "legacy" : // configure a jottacloud backend using legacy authentication
2021-05-04 13:13:12 +00:00
m . Set ( "configVersion" , fmt . Sprint ( legacyConfigVersion ) )
2021-05-04 11:27:50 +00:00
return fs . ConfigConfirm ( "legacy_api" , false , "config_machine_specific" , ` Do you want to create a machine specific API key ?
2021-04-29 08:28:18 +00:00
Rclone has it ' s own Jottacloud API KEY which works fine as long as one
only uses rclone on a single machine . When you want to use rclone with
this account on more than one machine it ' s recommended to create a
machine specific API key . These keys can NOT be shared between
machines . ` )
case "legacy_api" :
srv := rest . NewClient ( fshttp . NewClient ( ctx ) )
if config . Result == "true" {
deviceRegistration , err := registerDevice ( ctx , srv )
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "failed to register device: %w" , err )
2021-04-29 08:28:18 +00:00
}
m . Set ( configClientID , deviceRegistration . ClientID )
m . Set ( configClientSecret , obscure . MustObscure ( deviceRegistration . ClientSecret ) )
fs . Debugf ( nil , "Got clientID %q and clientSecret %q" , deviceRegistration . ClientID , deviceRegistration . ClientSecret )
}
2021-05-04 13:13:12 +00:00
return fs . ConfigInput ( "legacy_username" , "config_username" , "Username (e-mail address)" )
2021-04-29 08:28:18 +00:00
case "legacy_username" :
m . Set ( configUsername , config . Result )
2021-05-04 13:13:12 +00:00
return fs . ConfigPassword ( "legacy_password" , "config_password" , "Password (only used in setup, will not be stored)" )
2021-04-29 08:28:18 +00:00
case "legacy_password" :
m . Set ( "password" , config . Result )
m . Set ( "auth_code" , "" )
return fs . ConfigGoto ( "legacy_do_auth" )
case "legacy_auth_code" :
2022-05-16 16:11:45 +00:00
authCode := strings . ReplaceAll ( config . Result , "-" , "" ) // remove any "-" contained in the code so we have a 6 digit number
2021-04-29 08:28:18 +00:00
m . Set ( "auth_code" , authCode )
return fs . ConfigGoto ( "legacy_do_auth" )
case "legacy_do_auth" :
username , _ := m . Get ( configUsername )
password , _ := m . Get ( "password" )
2021-05-04 13:13:12 +00:00
password = obscure . MustReveal ( password )
2021-04-29 08:28:18 +00:00
authCode , _ := m . Get ( "auth_code" )
2021-05-04 13:13:12 +00:00
srv := rest . NewClient ( fshttp . NewClient ( ctx ) )
2021-04-29 08:28:18 +00:00
clientID , ok := m . Get ( configClientID )
if ! ok {
2021-05-04 13:13:12 +00:00
clientID = legacyClientID
2021-04-29 08:28:18 +00:00
}
clientSecret , ok := m . Get ( configClientSecret )
if ! ok {
2021-05-04 13:13:12 +00:00
clientSecret = legacyEncryptedClientSecret
2021-04-29 08:28:18 +00:00
}
2021-05-04 13:13:12 +00:00
oauthConfig := & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
AuthURL : legacyTokenURL ,
} ,
ClientID : clientID ,
ClientSecret : obscure . MustReveal ( clientSecret ) ,
}
token , err := doLegacyAuth ( ctx , srv , oauthConfig , username , password , authCode )
2021-04-29 08:28:18 +00:00
if err == errAuthCodeRequired {
2021-05-04 11:27:50 +00:00
return fs . ConfigInput ( "legacy_auth_code" , "config_auth_code" , "Verification Code\nThis account uses 2 factor authentication you will receive a verification code via SMS." )
2021-04-29 08:28:18 +00:00
}
m . Set ( "password" , "" )
m . Set ( "auth_code" , "" )
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "failed to get oauth token: %w" , err )
2021-04-29 08:28:18 +00:00
}
err = oauthutil . PutToken ( name , m , & token , true )
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "error while saving token: %w" , err )
2021-04-29 08:28:18 +00:00
}
return fs . ConfigGoto ( "choose_device" )
2023-09-01 13:55:32 +00:00
case "telia_se" : // telia_se cloud config
m . Set ( "configVersion" , fmt . Sprint ( configVersion ) )
m . Set ( configClientID , teliaseCloudClientID )
m . Set ( configTokenURL , teliaseCloudTokenURL )
return oauthutil . ConfigOut ( "choose_device" , & oauthutil . Options {
OAuth2Config : & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
AuthURL : teliaseCloudAuthURL ,
TokenURL : teliaseCloudTokenURL ,
} ,
ClientID : teliaseCloudClientID ,
Scopes : [ ] string { "openid" , "jotta-default" , "offline_access" } ,
RedirectURL : oauthutil . RedirectLocalhostURL ,
} ,
} )
case "telia_no" : // telia_no cloud config
2021-04-29 08:28:18 +00:00
m . Set ( "configVersion" , fmt . Sprint ( configVersion ) )
2023-09-01 13:55:32 +00:00
m . Set ( configClientID , telianoCloudClientID )
m . Set ( configTokenURL , telianoCloudTokenURL )
2021-04-29 08:28:18 +00:00
return oauthutil . ConfigOut ( "choose_device" , & oauthutil . Options {
OAuth2Config : & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
2023-09-01 13:55:32 +00:00
AuthURL : telianoCloudAuthURL ,
TokenURL : telianoCloudTokenURL ,
2021-04-29 08:28:18 +00:00
} ,
2023-09-01 13:55:32 +00:00
ClientID : telianoCloudClientID ,
2021-04-29 08:28:18 +00:00
Scopes : [ ] string { "openid" , "jotta-default" , "offline_access" } ,
RedirectURL : oauthutil . RedirectLocalhostURL ,
} ,
} )
2021-11-10 07:52:44 +00:00
case "tele2" : // tele2 cloud config
m . Set ( "configVersion" , fmt . Sprint ( configVersion ) )
m . Set ( configClientID , tele2CloudClientID )
m . Set ( configTokenURL , tele2CloudTokenURL )
return oauthutil . ConfigOut ( "choose_device" , & oauthutil . Options {
OAuth2Config : & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
AuthURL : tele2CloudAuthURL ,
TokenURL : tele2CloudTokenURL ,
} ,
ClientID : tele2CloudClientID ,
Scopes : [ ] string { "openid" , "jotta-default" , "offline_access" } ,
RedirectURL : oauthutil . RedirectLocalhostURL ,
} ,
} )
2023-07-02 10:16:07 +00:00
case "onlime" : // onlime cloud config
m . Set ( "configVersion" , fmt . Sprint ( configVersion ) )
m . Set ( configClientID , onlimeCloudClientID )
m . Set ( configTokenURL , onlimeCloudTokenURL )
return oauthutil . ConfigOut ( "choose_device" , & oauthutil . Options {
OAuth2Config : & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
AuthURL : onlimeCloudAuthURL ,
TokenURL : onlimeCloudTokenURL ,
} ,
ClientID : onlimeCloudClientID ,
Scopes : [ ] string { "openid" , "jotta-default" , "offline_access" } ,
RedirectURL : oauthutil . RedirectLocalhostURL ,
} ,
} )
2021-04-29 08:28:18 +00:00
case "choose_device" :
2022-01-18 19:59:50 +00:00
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 . ` )
2021-04-29 08:28:18 +00:00
case "choose_device_query" :
if config . Result != "true" {
m . Set ( configDevice , "" )
m . Set ( configMountpoint , "" )
return fs . ConfigGoto ( "end" )
}
oAuthClient , _ , err := getOAuthClient ( ctx , name , m )
if err != nil {
return nil , err
}
2022-07-12 20:10:04 +00:00
jfsSrv := rest . NewClient ( oAuthClient ) . SetRoot ( jfsURL )
apiSrv := rest . NewClient ( oAuthClient ) . SetRoot ( apiURL )
cust , err := getCustomerInfo ( ctx , apiSrv )
if err != nil {
return nil , err
2021-04-29 08:28:18 +00:00
}
2022-07-12 20:10:04 +00:00
acc , err := getDriveInfo ( ctx , jfsSrv , cust . Username )
2021-04-29 08:28:18 +00:00
if err != nil {
return nil , err
}
2022-01-18 19:59:50 +00:00
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 ] , ""
2021-04-29 08:28:18 +00:00
} )
case "choose_device_result" :
device := config . Result
oAuthClient , _ , err := getOAuthClient ( ctx , name , m )
if err != nil {
return nil , err
}
2022-01-18 20:06:22 +00:00
jfsSrv := rest . NewClient ( oAuthClient ) . SetRoot ( jfsURL )
2022-07-15 08:01:06 +00:00
apiSrv := rest . NewClient ( oAuthClient ) . SetRoot ( apiURL )
2021-04-29 08:28:18 +00:00
2022-07-15 08:01:06 +00:00
cust , err := getCustomerInfo ( ctx , apiSrv )
if err != nil {
return nil , err
}
2022-01-18 19:59:50 +00:00
2022-07-15 08:01:06 +00:00
acc , err := getDriveInfo ( ctx , jfsSrv , cust . Username )
2021-04-29 08:28:18 +00:00
if err != nil {
return nil , err
}
2022-01-18 19:59:50 +00:00
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 )
2022-07-15 08:01:06 +00:00
dev , err = createDevice ( ctx , jfsSrv , path . Join ( cust . Username , device ) )
2022-01-18 19:59:50 +00:00
if err != nil {
return nil , err
}
}
m . Set ( configDevice , device )
if ! isNew {
2022-07-15 08:01:06 +00:00
dev , err = getDeviceInfo ( ctx , jfsSrv , path . Join ( cust . Username , device ) )
2022-01-18 19:59:50 +00:00
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 ) {
2021-04-29 08:28:18 +00:00
return dev . MountPoints [ i ] . Name , ""
} )
case "choose_device_mountpoint" :
mountpoint := config . Result
2022-01-18 19:59:50 +00:00
oAuthClient , _ , err := getOAuthClient ( ctx , name , m )
if err != nil {
return nil , err
}
2022-01-18 20:06:22 +00:00
jfsSrv := rest . NewClient ( oAuthClient ) . SetRoot ( jfsURL )
2022-07-15 08:01:06 +00:00
apiSrv := rest . NewClient ( oAuthClient ) . SetRoot ( apiURL )
cust , err := getCustomerInfo ( ctx , apiSrv )
if err != nil {
return nil , err
}
2022-01-18 19:59:50 +00:00
device , _ := m . Get ( configDevice )
2022-07-15 08:01:06 +00:00
dev , err := getDeviceInfo ( ctx , jfsSrv , path . Join ( cust . Username , device ) )
2022-01-18 19:59:50 +00:00
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 )
2022-07-15 08:01:06 +00:00
_ , err := createMountPoint ( ctx , jfsSrv , path . Join ( cust . Username , device , mountpoint ) )
2022-01-18 19:59:50 +00:00
if err != nil {
return nil , err
}
}
2021-04-29 08:28:18 +00:00
m . Set ( configMountpoint , mountpoint )
2022-01-18 19:59:50 +00:00
2021-04-29 08:28:18 +00:00
return fs . ConfigGoto ( "end" )
case "end" :
// All the config flows end up here in case we need to carry on with something
return nil , nil
}
return nil , fmt . Errorf ( "unknown state %q" , config . State )
}
2018-08-01 23:02:35 +00:00
// Options defines the configuration for this backend
type Options struct {
2020-01-14 17:33:35 +00:00
Device string ` config:"device" `
Mountpoint string ` config:"mountpoint" `
MD5MemoryThreshold fs . SizeSuffix ` config:"md5_memory_limit" `
2020-04-11 10:11:20 +00:00
TrashedOnly bool ` config:"trashed_only" `
2020-01-14 17:33:35 +00:00
HardDelete bool ` config:"hard_delete" `
2021-07-01 21:17:05 +00:00
NoVersions bool ` config:"no_versions" `
2020-01-14 17:33:35 +00:00
UploadThreshold fs . SizeSuffix ` config:"upload_resume_limit" `
Enc encoder . MultiEncoder ` config:"encoding" `
2018-08-01 23:02:35 +00:00
}
// Fs represents a remote jottacloud
type Fs struct {
2022-01-18 20:48:44 +00:00
name string
root string
user string
opt Options
features * fs . Features
fileEndpoint string
allocateEndpoint string
jfsSrv * rest . Client
apiSrv * rest . Client
pacer * fs . Pacer
tokenRenewer * oauthutil . Renew // renew the token on expiry
2018-08-01 23:02:35 +00:00
}
// Object describes a jottacloud object
//
// Will definitely have info but maybe not meta
type Object struct {
fs * Fs
remote string
hasMetaData bool
size int64
2022-08-04 18:48:13 +00:00
createTime time . Time
2018-08-01 23:02:35 +00:00
modTime time . Time
2022-08-04 18:48:13 +00:00
updateTime time . Time
2018-08-01 23:02:35 +00:00
md5 string
2018-08-14 22:33:58 +00:00
mimeType string
2018-08-01 23:02:35 +00:00
}
// ------------------------------------------------------------
// 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 converts this Fs to a string
func ( f * Fs ) String ( ) string {
return fmt . Sprintf ( "jottacloud root '%s'" , f . root )
}
// Features returns the optional features of this Fs
func ( f * Fs ) Features ( ) * fs . Features {
return f . features
}
2021-05-31 07:31:35 +00:00
// joinPath joins two path/url elements
//
// Does not perform clean on the result like path.Join does,
// which breaks urls by changing prefix "https://" into "https:/".
func joinPath ( base string , rel string ) string {
if rel == "" {
return base
}
if strings . HasSuffix ( base , "/" ) {
return base + strings . TrimPrefix ( rel , "/" )
}
if strings . HasPrefix ( rel , "/" ) {
return strings . TrimSuffix ( base , "/" ) + rel
}
return base + "/" + rel
2018-08-01 23:02:35 +00:00
}
// 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
2021-03-11 14:44:01 +00:00
func shouldRetry ( ctx context . Context , resp * http . Response , err error ) ( bool , error ) {
if fserrors . ContextError ( ctx , & err ) {
return false , err
}
2018-08-01 23:02:35 +00:00
return fserrors . ShouldRetry ( err ) || fserrors . ShouldRetryHTTP ( resp , retryErrorCodes ) , err
}
2020-06-11 11:02:28 +00:00
// registerDevice register a new device for use with the jottacloud API
func registerDevice ( ctx context . Context , srv * rest . Client ) ( reg * api . DeviceRegistrationResponse , err error ) {
// random generator to generate random device names
seededRand := rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) )
randonDeviceNamePartLength := 21
randomDeviceNamePart := make ( [ ] byte , randonDeviceNamePartLength )
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
for i := range randomDeviceNamePart {
randomDeviceNamePart [ i ] = charset [ seededRand . Intn ( len ( charset ) ) ]
}
randomDeviceName := "rclone-" + string ( randomDeviceNamePart )
fs . Debugf ( nil , "Trying to register device '%s'" , randomDeviceName )
values := url . Values { }
values . Set ( "device_id" , randomDeviceName )
opts := rest . Opts {
Method : "POST" ,
2021-05-04 13:13:12 +00:00
RootURL : legacyRegisterURL ,
2020-06-11 11:02:28 +00:00
ContentType : "application/x-www-form-urlencoded" ,
ExtraHeaders : map [ string ] string { "Authorization" : "Bearer c2xrZmpoYWRsZmFramhkc2xma2phaHNkbGZramhhc2xkZmtqaGFzZGxrZmpobGtq" } ,
Parameters : values ,
}
var deviceRegistration * api . DeviceRegistrationResponse
_ , err = srv . CallJSON ( ctx , & opts , nil , & deviceRegistration )
return deviceRegistration , err
}
2021-04-29 08:28:18 +00:00
var errAuthCodeRequired = errors . New ( "auth code required" )
2021-05-04 13:13:12 +00:00
// doLegacyAuth runs the actual token request for V1 authentication
2021-04-29 08:28:18 +00:00
//
// Call this first with blank authCode. If errAuthCodeRequired is
// returned then call it again with an authCode
2021-05-04 13:13:12 +00:00
func doLegacyAuth ( ctx context . Context , srv * rest . Client , oauthConfig * oauth2 . Config , username , password , authCode string ) ( token oauth2 . Token , err error ) {
2020-06-11 11:02:28 +00:00
// prepare out token request with username and password
values := url . Values { }
values . Set ( "grant_type" , "PASSWORD" )
values . Set ( "password" , password )
values . Set ( "username" , username )
values . Set ( "client_id" , oauthConfig . ClientID )
values . Set ( "client_secret" , oauthConfig . ClientSecret )
opts := rest . Opts {
Method : "POST" ,
RootURL : oauthConfig . Endpoint . AuthURL ,
ContentType : "application/x-www-form-urlencoded" ,
Parameters : values ,
}
2021-04-29 08:28:18 +00:00
if authCode != "" {
opts . ExtraHeaders = make ( map [ string ] string )
opts . ExtraHeaders [ "X-Jottacloud-Otp" ] = authCode
}
2020-06-11 11:02:28 +00:00
// do the first request
var jsonToken api . TokenJSON
resp , err := srv . CallJSON ( ctx , & opts , nil , & jsonToken )
2021-04-29 08:28:18 +00:00
if err != nil && authCode == "" {
2020-06-11 11:02:28 +00:00
// if 2fa is enabled the first request is expected to fail. We will do another request with the 2fa code as an additional http header
if resp != nil {
if resp . Header . Get ( "X-JottaCloud-OTP" ) == "required; SMS" {
2021-04-29 08:28:18 +00:00
return token , errAuthCodeRequired
2020-06-11 11:02:28 +00:00
}
}
}
token . AccessToken = jsonToken . AccessToken
token . RefreshToken = jsonToken . RefreshToken
token . TokenType = jsonToken . TokenType
token . Expiry = time . Now ( ) . Add ( time . Duration ( jsonToken . ExpiresIn ) * time . Second )
return token , err
}
2021-05-04 13:13:12 +00:00
// doTokenAuth runs the actual token request for V2 authentication
2022-07-12 20:10:04 +00:00
func doTokenAuth ( ctx context . Context , apiSrv * rest . Client , loginTokenBase64 string ) ( token oauth2 . Token , tokenEndpoint string , err error ) {
2020-02-22 00:15:14 +00:00
loginTokenBytes , err := base64 . RawURLEncoding . DecodeString ( loginTokenBase64 )
2019-11-19 23:10:38 +00:00
if err != nil {
2022-07-12 20:10:04 +00:00
return token , "" , err
2019-08-14 17:39:19 +00:00
}
2019-12-28 16:45:04 +00:00
// decode login token
2019-11-19 23:10:38 +00:00
var loginToken api . LoginToken
decoder := json . NewDecoder ( bytes . NewReader ( loginTokenBytes ) )
err = decoder . Decode ( & loginToken )
if err != nil {
2022-07-12 20:10:04 +00:00
return token , "" , err
2019-11-19 23:10:38 +00:00
}
2019-08-14 17:39:19 +00:00
2019-12-28 16:45:04 +00:00
// retrieve endpoint urls
2018-08-01 23:02:35 +00:00
opts := rest . Opts {
2019-12-28 16:45:04 +00:00
Method : "GET" ,
RootURL : loginToken . WellKnownLink ,
2019-11-19 23:10:38 +00:00
}
2019-12-28 16:45:04 +00:00
var wellKnown api . WellKnown
2021-05-04 13:13:12 +00:00
_ , err = apiSrv . CallJSON ( ctx , & opts , nil , & wellKnown )
2019-11-19 23:10:38 +00:00
if err != nil {
2022-07-12 20:10:04 +00:00
return token , "" , err
2018-08-01 23:02:35 +00:00
}
2019-08-14 17:39:19 +00:00
// prepare out token request with username and password
values := url . Values { }
2021-05-04 13:13:12 +00:00
values . Set ( "client_id" , defaultClientID )
2019-11-19 23:10:38 +00:00
values . Set ( "grant_type" , "password" )
values . Set ( "password" , loginToken . AuthToken )
2022-04-22 11:52:00 +00:00
values . Set ( "scope" , "openid offline_access" )
2019-11-19 23:10:38 +00:00
values . Set ( "username" , loginToken . Username )
values . Encode ( )
opts = rest . Opts {
2019-08-14 17:39:19 +00:00
Method : "POST" ,
2021-05-04 13:13:12 +00:00
RootURL : wellKnown . TokenEndpoint ,
2019-08-14 17:39:19 +00:00
ContentType : "application/x-www-form-urlencoded" ,
2019-11-19 23:10:38 +00:00
Body : strings . NewReader ( values . Encode ( ) ) ,
2019-08-14 17:39:19 +00:00
}
// do the first request
var jsonToken api . TokenJSON
2021-05-04 13:13:12 +00:00
_ , err = apiSrv . CallJSON ( ctx , & opts , nil , & jsonToken )
2019-08-14 17:39:19 +00:00
if err != nil {
2022-07-12 20:10:04 +00:00
return token , "" , err
2018-08-01 23:02:35 +00:00
}
2019-08-14 17:39:19 +00:00
token . AccessToken = jsonToken . AccessToken
token . RefreshToken = jsonToken . RefreshToken
token . TokenType = jsonToken . TokenType
token . Expiry = time . Now ( ) . Add ( time . Duration ( jsonToken . ExpiresIn ) * time . Second )
2022-07-12 20:10:04 +00:00
return token , wellKnown . TokenEndpoint , err
2019-08-14 17:39:19 +00:00
}
// getCustomerInfo queries general information about the account
2021-05-04 13:13:12 +00:00
func getCustomerInfo ( ctx context . Context , apiSrv * rest . Client ) ( info * api . CustomerInfo , err error ) {
2019-08-13 13:28:46 +00:00
opts := rest . Opts {
Method : "GET" ,
Path : "account/v1/customer" ,
}
2021-05-04 13:13:12 +00:00
_ , err = apiSrv . CallJSON ( ctx , & opts , nil , & info )
2019-08-13 13:28:46 +00:00
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "couldn't get customer info: %w" , err )
2019-08-13 13:28:46 +00:00
}
return info , nil
}
2019-08-14 17:39:19 +00:00
// getDriveInfo queries general information about the account and the available devices and mountpoints.
2019-09-04 19:00:37 +00:00
func getDriveInfo ( ctx context . Context , srv * rest . Client , username string ) ( info * api . DriveInfo , err error ) {
2018-08-01 23:02:35 +00:00
opts := rest . Opts {
Method : "GET" ,
2019-08-13 13:28:46 +00:00
Path : username ,
2018-08-01 23:02:35 +00:00
}
2019-09-04 19:00:37 +00:00
_ , err = srv . CallXML ( ctx , & opts , nil , & info )
2019-05-17 00:31:59 +00:00
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "couldn't get drive info: %w" , err )
2019-05-17 00:31:59 +00:00
}
return info , nil
}
// getDeviceInfo queries Information about a jottacloud device
2019-09-04 19:00:37 +00:00
func getDeviceInfo ( ctx context . Context , srv * rest . Client , path string ) ( info * api . JottaDevice , err error ) {
2019-05-17 00:31:59 +00:00
opts := rest . Opts {
Method : "GET" ,
Path : urlPathEscape ( path ) ,
}
2019-09-04 19:00:37 +00:00
_ , err = srv . CallXML ( ctx , & opts , nil , & info )
2018-08-01 23:02:35 +00:00
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "couldn't get device info: %w" , err )
2018-08-01 23:02:35 +00:00
}
2018-08-14 23:12:20 +00:00
return info , nil
}
2022-01-18 19:59:50 +00:00
// 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
}
2022-01-18 20:48:44 +00:00
// setEndpoints generates the API endpoints
func ( f * Fs ) setEndpoints ( ) {
2019-05-17 00:31:59 +00:00
if f . opt . Device == "" {
f . opt . Device = defaultDevice
}
if f . opt . Mountpoint == "" {
f . opt . Mountpoint = defaultMountpoint
}
2022-01-18 20:48:44 +00:00
f . fileEndpoint = path . Join ( f . user , f . opt . Device , f . opt . Mountpoint )
f . allocateEndpoint = path . Join ( "/jfs" , f . opt . Device , f . opt . Mountpoint )
2018-08-01 23:02:35 +00:00
}
2019-08-14 17:39:19 +00:00
// readMetaDataForPath reads the metadata from the path
2019-09-04 19:00:37 +00:00
func ( f * Fs ) readMetaDataForPath ( ctx context . Context , path string ) ( info * api . JottaFile , err error ) {
2019-08-14 17:39:19 +00:00
opts := rest . Opts {
Method : "GET" ,
Path : f . filePath ( path ) ,
}
var result api . JottaFile
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err = f . jfsSrv . CallXML ( ctx , & opts , nil , & result )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2019-08-14 17:39:19 +00:00
} )
if apiErr , ok := err . ( * api . Error ) ; ok {
// does not exist
if apiErr . StatusCode == http . StatusNotFound {
return nil , fs . ErrorObjectNotFound
}
}
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "read metadata failed: %w" , err )
2019-08-14 17:39:19 +00:00
}
2021-09-06 12:54:08 +00:00
if result . XMLName . Local == "folder" {
return nil , fs . ErrorIsDir
} else if result . XMLName . Local != "file" {
2019-08-14 17:39:19 +00:00
return nil , fs . ErrorNotAFile
}
return & result , nil
}
2018-08-01 23:02:35 +00:00
// errorHandler parses a non 2xx error response into an error
func errorHandler ( resp * http . Response ) error {
// Decode error response
errResponse := new ( api . Error )
err := rest . DecodeXML ( resp , & errResponse )
if err != nil {
fs . Debugf ( nil , "Couldn't decode error response: %v" , err )
}
if errResponse . Message == "" {
errResponse . Message = resp . Status
}
if errResponse . StatusCode == 0 {
errResponse . StatusCode = resp . StatusCode
}
return errResponse
}
2020-05-20 10:39:20 +00:00
// Jottacloud wants '+' to be URL encoded even though the RFC states it's not reserved
2018-09-16 20:30:20 +00:00
func urlPathEscape ( in string ) string {
2022-05-16 16:11:45 +00:00
return strings . ReplaceAll ( rest . URLPathEscape ( in ) , "+" , "%2B" )
2018-09-16 20:30:20 +00:00
}
2018-09-06 13:13:38 +00:00
// filePathRaw returns an unescaped file path (f.root, file)
2022-01-18 20:48:44 +00:00
// Optionally made absolute by prefixing with "/", typically required when used
// as request parameter instead of the path (which is relative to some root url).
func ( f * Fs ) filePathRaw ( file string , absolute bool ) string {
prefix := ""
if absolute {
prefix = "/"
}
return path . Join ( prefix , f . fileEndpoint , f . opt . Enc . FromStandardPath ( path . Join ( f . root , file ) ) )
2018-09-06 13:13:38 +00:00
}
2020-05-20 10:39:20 +00:00
// filePath returns an escaped file path (f.root, file)
2018-08-01 23:02:35 +00:00
func ( f * Fs ) filePath ( file string ) string {
2022-01-18 20:48:44 +00:00
return urlPathEscape ( f . filePathRaw ( file , false ) )
2018-08-01 23:02:35 +00:00
}
2022-01-18 20:48:44 +00:00
// allocatePathRaw returns an unescaped allocate file path (f.root, file)
// Optionally made absolute by prefixing with "/", typically required when used
// as request parameter instead of the path (which is relative to some root url).
func ( f * Fs ) allocatePathRaw ( file string , absolute bool ) string {
prefix := ""
if absolute {
prefix = "/"
}
return path . Join ( prefix , f . allocateEndpoint , f . opt . Enc . FromStandardPath ( path . Join ( f . root , file ) ) )
2022-01-18 19:59:50 +00:00
}
2020-06-11 11:02:28 +00:00
// 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
//
// This filter catches all refresh requests, reads the body,
// changes the case and then sends it on
func grantTypeFilter ( req * http . Request ) {
2021-05-04 13:13:12 +00:00
if legacyTokenURL == req . URL . String ( ) {
2020-06-11 11:02:28 +00:00
// read the entire body
2022-08-20 14:38:02 +00:00
refreshBody , err := io . ReadAll ( req . Body )
2020-06-11 11:02:28 +00:00
if err != nil {
return
}
_ = req . Body . Close ( )
// make the refresh token upper case
refreshBody = [ ] byte ( strings . Replace ( string ( refreshBody ) , "grant_type=refresh_token" , "grant_type=REFRESH_TOKEN" , 1 ) )
// set the new ReadCloser (with a dummy Close())
2022-08-20 14:38:02 +00:00
req . Body = io . NopCloser ( bytes . NewReader ( refreshBody ) )
2020-06-11 11:02:28 +00:00
}
}
2021-04-29 08:28:18 +00:00
func getOAuthClient ( ctx context . Context , name string , m configmap . Mapper ) ( oAuthClient * http . Client , ts * oauthutil . TokenSource , err error ) {
2019-12-28 16:45:04 +00:00
// Check config version
2020-06-11 11:02:28 +00:00
var ver int
version , ok := m . Get ( "configVersion" )
if ok {
ver , err = strconv . Atoi ( version )
2019-11-19 23:10:38 +00:00
if err != nil {
2022-01-19 09:26:05 +00:00
return nil , nil , errors . New ( "failed to parse config version" )
2019-11-19 23:10:38 +00:00
}
2021-05-04 13:13:12 +00:00
ok = ( ver == configVersion ) || ( ver == legacyConfigVersion )
2019-04-07 14:12:32 +00:00
}
if ! ok {
2022-01-19 09:26:05 +00:00
return nil , nil , errors . New ( "outdated config - please reconfigure this backend" )
2019-04-07 14:12:32 +00:00
}
2020-11-13 15:24:43 +00:00
baseClient := fshttp . NewClient ( ctx )
2021-05-04 13:13:12 +00:00
oauthConfig := & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
AuthURL : defaultTokenURL ,
TokenURL : defaultTokenURL ,
} ,
}
2020-06-11 11:02:28 +00:00
if ver == configVersion {
2021-05-04 13:13:12 +00:00
oauthConfig . ClientID = defaultClientID
2020-06-11 11:02:28 +00:00
// if custom endpoints are set use them else stick with defaults
if tokenURL , ok := m . Get ( configTokenURL ) ; ok {
oauthConfig . Endpoint . TokenURL = tokenURL
// jottacloud is weird. we need to use the tokenURL as authURL
oauthConfig . Endpoint . AuthURL = tokenURL
}
2021-05-04 13:13:12 +00:00
} else if ver == legacyConfigVersion {
2020-06-11 11:02:28 +00:00
clientID , ok := m . Get ( configClientID )
if ! ok {
2021-05-04 13:13:12 +00:00
clientID = legacyClientID
2020-06-11 11:02:28 +00:00
}
clientSecret , ok := m . Get ( configClientSecret )
if ! ok {
2021-05-04 13:13:12 +00:00
clientSecret = legacyEncryptedClientSecret
2020-06-11 11:02:28 +00:00
}
oauthConfig . ClientID = clientID
oauthConfig . ClientSecret = obscure . MustReveal ( clientSecret )
2021-05-04 13:13:12 +00:00
oauthConfig . Endpoint . TokenURL = legacyTokenURL
oauthConfig . Endpoint . AuthURL = legacyTokenURL
2020-06-11 11:02:28 +00:00
// add the request filter to fix token refresh
if do , ok := baseClient . Transport . ( interface {
SetRequestFilter ( f func ( req * http . Request ) )
} ) ; ok {
do . SetRequestFilter ( grantTypeFilter )
} else {
fs . Debugf ( name + ":" , "Couldn't add request filter - uploads will fail" )
}
2019-12-28 16:45:04 +00:00
}
2019-11-19 23:10:38 +00:00
2019-12-28 16:45:04 +00:00
// Create OAuth Client
2021-04-29 08:28:18 +00:00
oAuthClient , ts , err = oauthutil . NewClientWithBaseClient ( ctx , name , m , oauthConfig , baseClient )
2018-08-22 20:26:18 +00:00
if err != nil {
2022-01-19 09:26:05 +00:00
return nil , nil , fmt . Errorf ( "failed to configure Jottacloud oauth client: %w" , err )
2021-04-29 08:28:18 +00:00
}
return oAuthClient , ts , nil
}
// NewFs constructs an Fs from the path, container:path
func NewFs ( ctx context . Context , name , root string , m configmap . Mapper ) ( fs . Fs , error ) {
// Parse config into Options struct
opt := new ( Options )
err := configstruct . Set ( m , opt )
if err != nil {
return nil , err
}
oAuthClient , ts , err := getOAuthClient ( ctx , name , m )
if err != nil {
return nil , err
2018-08-22 20:26:18 +00:00
}
2019-12-28 16:45:04 +00:00
rootIsDir := strings . HasSuffix ( root , "/" )
2021-05-31 07:31:35 +00:00
root = strings . Trim ( root , "/" )
2019-12-28 16:45:04 +00:00
2018-08-01 23:02:35 +00:00
f := & Fs {
2019-01-28 20:50:51 +00:00
name : name ,
root : root ,
opt : * opt ,
2022-01-18 20:06:22 +00:00
jfsSrv : rest . NewClient ( oAuthClient ) . SetRoot ( jfsURL ) ,
2018-08-22 20:26:18 +00:00
apiSrv : rest . NewClient ( oAuthClient ) . SetRoot ( apiURL ) ,
2020-11-05 11:33:32 +00:00
pacer : fs . NewPacer ( ctx , pacer . NewDefault ( pacer . MinSleep ( minSleep ) , pacer . MaxSleep ( maxSleep ) , pacer . DecayConstant ( decayConstant ) ) ) ,
2018-08-01 23:02:35 +00:00
}
f . features = ( & fs . Features {
CaseInsensitive : true ,
CanHaveEmptyDirectories : true ,
2018-08-14 23:12:20 +00:00
ReadMimeType : true ,
2020-11-29 16:27:20 +00:00
WriteMimeType : false ,
2022-08-04 18:48:13 +00:00
ReadMetadata : true ,
WriteMetadata : true ,
UserMetadata : false ,
2020-11-05 16:00:40 +00:00
} ) . Fill ( ctx , f )
2022-01-18 20:06:22 +00:00
f . jfsSrv . SetErrorHandler ( errorHandler )
2020-04-11 10:11:20 +00:00
if opt . TrashedOnly { // we cannot support showing Trashed Files when using ListR right now
f . features . ListR = nil
}
2018-08-22 20:26:18 +00:00
// Renew the token in the background
f . tokenRenewer = oauthutil . NewRenew ( f . String ( ) , ts , func ( ) error {
2019-09-04 19:00:37 +00:00
_ , err := f . readMetaDataForPath ( ctx , "" )
2021-09-06 12:54:08 +00:00
if err == fs . ErrorNotAFile || err == fs . ErrorIsDir {
2021-01-12 17:09:44 +00:00
err = nil
}
2018-08-22 20:26:18 +00:00
return err
} )
2022-07-12 20:10:04 +00:00
cust , err := getCustomerInfo ( ctx , f . apiSrv )
if err != nil {
return nil , err
2018-08-01 23:02:35 +00:00
}
2022-07-12 20:10:04 +00:00
f . user = cust . Username
2022-01-18 20:48:44 +00:00
f . setEndpoints ( )
2018-08-01 23:02:35 +00:00
if root != "" && ! rootIsDir {
// Check to see if the root actually an existing file
remote := path . Base ( root )
f . root = path . Dir ( root )
if f . root == "." {
f . root = ""
}
2019-06-17 08:34:30 +00:00
_ , err := f . NewObject ( context . TODO ( ) , remote )
2018-08-01 23:02:35 +00:00
if err != nil {
2021-11-04 10:12:57 +00:00
if errors . Is ( err , fs . ErrorObjectNotFound ) || errors . Is ( err , fs . ErrorNotAFile ) || errors . Is ( err , fs . ErrorIsDir ) {
2018-08-01 23:02:35 +00:00
// File doesn't exist so return old f
f . root = root
return f , nil
}
return nil , err
}
// return an error with an fs which points to the parent
return f , fs . ErrorIsFile
}
return f , nil
}
// Return an Object from a path
//
// If it can't be found it returns the error fs.ErrorObjectNotFound.
2019-09-04 19:00:37 +00:00
func ( f * Fs ) newObjectWithInfo ( ctx context . Context , remote string , info * api . JottaFile ) ( fs . Object , error ) {
2018-08-01 23:02:35 +00:00
o := & Object {
fs : f ,
remote : remote ,
}
var err error
if info != nil {
2021-09-29 13:34:59 +00:00
if ! f . validFile ( info ) {
return nil , fs . ErrorObjectNotFound
2021-09-29 13:31:08 +00:00
}
err = o . setMetaData ( info ) // sets the info
2018-08-01 23:02:35 +00:00
} else {
2019-09-04 19:00:37 +00:00
err = o . readMetaData ( ctx , false ) // reads info and meta, returning an error
2018-08-01 23:02:35 +00:00
}
if err != nil {
return nil , err
}
return o , nil
}
// NewObject finds the Object at remote. If it can't be found
// it returns the error fs.ErrorObjectNotFound.
2019-06-17 08:34:30 +00:00
func ( f * Fs ) NewObject ( ctx context . Context , remote string ) ( fs . Object , error ) {
2019-09-04 19:00:37 +00:00
return f . newObjectWithInfo ( ctx , remote , nil )
2018-08-01 23:02:35 +00:00
}
// CreateDir makes a directory
2019-09-04 19:00:37 +00:00
func ( f * Fs ) CreateDir ( ctx context . Context , path string ) ( jf * api . JottaFolder , err error ) {
2018-08-01 23:02:35 +00:00
// fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, leaf)
var resp * http . Response
opts := rest . Opts {
Method : "POST" ,
Path : f . filePath ( path ) ,
Parameters : url . Values { } ,
}
opts . Parameters . Set ( "mkDir" , "true" )
err = f . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err = f . jfsSrv . CallXML ( ctx , & opts , nil , & jf )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2018-08-01 23:02:35 +00:00
} )
if err != nil {
//fmt.Printf("...Error %v\n", err)
return nil , err
}
// fmt.Printf("...Id %q\n", *info.Id)
return jf , 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.
2019-06-17 08:34:30 +00:00
func ( f * Fs ) List ( ctx context . Context , dir string ) ( entries fs . DirEntries , err error ) {
2018-08-01 23:02:35 +00:00
opts := rest . Opts {
Method : "GET" ,
Path : f . filePath ( dir ) ,
}
var resp * http . Response
var result api . JottaFolder
err = f . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err = f . jfsSrv . CallXML ( ctx , & opts , nil , & result )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2018-08-01 23:02:35 +00:00
} )
if err != nil {
if apiErr , ok := err . ( * api . Error ) ; ok {
// does not exist
if apiErr . StatusCode == http . StatusNotFound {
return nil , fs . ErrorDirNotFound
}
}
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "couldn't list files: %w" , err )
2018-08-01 23:02:35 +00:00
}
2021-09-29 13:34:59 +00:00
if ! f . validFolder ( & result ) {
2018-08-01 23:02:35 +00:00
return nil , fs . ErrorDirNotFound
}
for i := range result . Folders {
item := & result . Folders [ i ]
2021-09-29 13:34:59 +00:00
if f . validFolder ( item ) {
remote := path . Join ( dir , f . opt . Enc . ToStandardName ( item . Name ) )
d := fs . NewDir ( remote , time . Time ( item . ModifiedAt ) )
entries = append ( entries , d )
2018-08-01 23:02:35 +00:00
}
}
for i := range result . Files {
item := & result . Files [ i ]
2021-09-29 13:34:59 +00:00
if f . validFile ( item ) {
remote := path . Join ( dir , f . opt . Enc . ToStandardName ( item . Name ) )
if o , err := f . newObjectWithInfo ( ctx , remote , item ) ; err == nil {
entries = append ( entries , o )
2020-04-11 10:11:20 +00:00
}
2018-08-01 23:02:35 +00:00
}
}
return entries , nil
}
2022-04-27 10:47:53 +00:00
func parseListRStream ( ctx context . Context , r io . Reader , filesystem * Fs , callback func ( fs . DirEntry ) error ) error {
2021-11-30 09:21:05 +00:00
type stats struct {
Folders int ` xml:"folders" `
Files int ` xml:"files" `
}
var expected , actual stats
type xmlFile struct {
2022-04-24 17:34:29 +00:00
Path string ` xml:"path" `
Name string ` xml:"filename" `
Checksum string ` xml:"md5" `
Size int64 ` xml:"size" `
Modified api . Rfc3339Time ` xml:"modified" ` // Note: Liststream response includes 3 decimal milliseconds, but we ignore them since there is second precision everywhere else
Created api . Rfc3339Time ` xml:"created" `
2021-11-30 09:21:05 +00:00
}
type xmlFolder struct {
Path string ` xml:"path" `
}
addFolder := func ( path string ) error {
return callback ( fs . NewDir ( filesystem . opt . Enc . ToStandardPath ( path ) , time . Time { } ) )
}
addFile := func ( f * xmlFile ) error {
return callback ( & Object {
hasMetaData : true ,
fs : filesystem ,
remote : filesystem . opt . Enc . ToStandardPath ( path . Join ( f . Path , f . Name ) ) ,
size : f . Size ,
md5 : f . Checksum ,
2022-08-04 18:48:13 +00:00
createTime : time . Time ( f . Created ) ,
2021-11-30 09:21:05 +00:00
modTime : time . Time ( f . Modified ) ,
} )
}
2022-04-27 10:47:53 +00:00
// liststream paths are /mountpoint/root/path
// so the returned paths should have /mountpoint/root/ trimmed
// as the caller is expecting path.
pathPrefix := filesystem . opt . Enc . FromStandardPath ( path . Join ( "/" , filesystem . opt . Mountpoint , filesystem . root ) )
2021-11-30 09:21:05 +00:00
trimPathPrefix := func ( p string ) string {
2022-04-27 10:47:53 +00:00
p = strings . TrimPrefix ( p , pathPrefix )
2021-11-30 09:21:05 +00:00
p = strings . TrimPrefix ( p , "/" )
return p
}
uniqueFolders := map [ string ] bool { }
decoder := xml . NewDecoder ( r )
for {
t , err := decoder . Token ( )
if err != nil {
if err != io . EOF {
return err
}
break
2018-09-06 13:13:38 +00:00
}
2021-11-30 09:21:05 +00:00
switch se := t . ( type ) {
case xml . StartElement :
switch se . Name . Local {
case "file" :
var f xmlFile
if err := decoder . DecodeElement ( & f , & se ) ; err != nil {
2018-09-08 22:12:47 +00:00
return err
}
2021-11-30 09:21:05 +00:00
f . Path = trimPathPrefix ( f . Path )
actual . Files ++
if ! uniqueFolders [ f . Path ] {
uniqueFolders [ f . Path ] = true
actual . Folders ++
if err := addFolder ( f . Path ) ; err != nil {
return err
}
}
if err := addFile ( & f ) ; err != nil {
return err
}
case "folder" :
var f xmlFolder
if err := decoder . DecodeElement ( & f , & se ) ; err != nil {
return err
}
f . Path = trimPathPrefix ( f . Path )
uniqueFolders [ f . Path ] = true
actual . Folders ++
if err := addFolder ( f . Path ) ; err != nil {
2021-09-29 13:34:59 +00:00
return err
}
2021-11-30 09:21:05 +00:00
case "stats" :
if err := decoder . DecodeElement ( & expected , & se ) ; err != nil {
2021-09-29 13:34:59 +00:00
return err
}
2018-09-06 13:13:38 +00:00
}
}
}
2021-11-30 09:21:05 +00:00
if expected . Folders != actual . Folders ||
expected . Files != actual . Files {
2022-01-19 09:26:05 +00:00
return fmt . Errorf ( "invalid result from listStream: expected[%#v] != actual[%#v]" , expected , actual )
2021-11-30 09:21:05 +00:00
}
2018-09-06 13:13:38 +00:00
return nil
}
// ListR lists the objects and directories of the Fs starting
// from dir recursively into out.
//
// dir should be "" to start from the root, and should not
// have trailing slashes.
2019-06-17 08:34:30 +00:00
func ( f * Fs ) ListR ( ctx context . Context , dir string , callback fs . ListRCallback ) ( err error ) {
2018-09-06 13:13:38 +00:00
opts := rest . Opts {
Method : "GET" ,
Path : f . filePath ( dir ) ,
Parameters : url . Values { } ,
}
2021-11-30 09:21:05 +00:00
opts . Parameters . Set ( "mode" , "liststream" )
list := walk . NewListRHelper ( callback )
2018-09-06 13:13:38 +00:00
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err = f . jfsSrv . Call ( ctx , & opts )
2021-11-30 09:21:05 +00:00
if err != nil {
return shouldRetry ( ctx , resp , err )
}
2022-04-27 10:47:53 +00:00
err = parseListRStream ( ctx , resp . Body , f , func ( d fs . DirEntry ) error {
2021-11-30 09:21:05 +00:00
if d . Remote ( ) == dir {
return nil
}
return list . Add ( d )
} )
_ = resp . Body . Close ( )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2018-09-06 13:13:38 +00:00
} )
if err != nil {
if apiErr , ok := err . ( * api . Error ) ; ok {
// does not exist
if apiErr . StatusCode == http . StatusNotFound {
return fs . ErrorDirNotFound
}
}
2021-11-04 10:12:57 +00:00
return fmt . Errorf ( "couldn't list files: %w" , err )
2018-09-06 13:13:38 +00:00
}
if err != nil {
return err
}
return list . Flush ( )
}
2018-08-01 23:02:35 +00:00
// Creates from the parameters passed in a half finished Object which
// must have setMetaData called on it
//
// Used to create new objects
func ( f * Fs ) createObject ( remote string , modTime time . Time , size int64 ) ( o * Object ) {
// Temporary Object under construction
o = & Object {
fs : f ,
remote : remote ,
size : size ,
modTime : modTime ,
}
return o
}
// Put the object
//
2022-08-05 15:35:41 +00:00
// Copy the reader in to the new object which is returned.
2018-08-01 23:02:35 +00:00
//
// The new object may have been created if an error is returned
2019-06-17 08:34:30 +00:00
func ( f * Fs ) Put ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
o := f . createObject ( src . Remote ( ) , src . ModTime ( ctx ) , src . Size ( ) )
return o , o . Update ( ctx , in , src , options ... )
2018-08-01 23:02:35 +00:00
}
// mkParentDir makes the parent of the native path dirPath if
// necessary and any directories above that
2019-06-17 08:34:30 +00:00
func ( f * Fs ) mkParentDir ( ctx context . Context , dirPath string ) error {
2018-08-01 23:02:35 +00:00
// defer log.Trace(dirPath, "")("")
// chop off trailing / if it exists
2022-06-08 20:25:17 +00:00
parent := path . Dir ( strings . TrimSuffix ( dirPath , "/" ) )
2018-08-01 23:02:35 +00:00
if parent == "." {
parent = ""
}
2019-06-17 08:34:30 +00:00
return f . Mkdir ( ctx , parent )
2018-08-01 23:02:35 +00:00
}
// Mkdir creates the container if it doesn't exist
2019-06-17 08:34:30 +00:00
func ( f * Fs ) Mkdir ( ctx context . Context , dir string ) error {
2019-09-04 19:00:37 +00:00
_ , err := f . CreateDir ( ctx , dir )
2018-08-01 23:02:35 +00:00
return err
}
// purgeCheck removes the root directory, if check is set then it
// refuses to do so if it has anything in
2019-06-17 08:34:30 +00:00
func ( f * Fs ) purgeCheck ( ctx context . Context , dir string , check bool ) ( err error ) {
2018-08-01 23:02:35 +00:00
root := path . Join ( f . root , dir )
if root == "" {
return errors . New ( "can't purge root directory" )
}
// check that the directory exists
2019-06-17 08:34:30 +00:00
entries , err := f . List ( ctx , dir )
2018-08-01 23:02:35 +00:00
if err != nil {
return err
}
if check {
if len ( entries ) != 0 {
return fs . ErrorDirectoryNotEmpty
}
}
opts := rest . Opts {
Method : "POST" ,
Path : f . filePath ( dir ) ,
Parameters : url . Values { } ,
NoResponse : true ,
}
2018-09-07 11:58:18 +00:00
if f . opt . HardDelete {
opts . Parameters . Set ( "rmDir" , "true" )
} else {
opts . Parameters . Set ( "dlDir" , "true" )
}
2018-08-01 23:02:35 +00:00
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err = f . jfsSrv . Call ( ctx , & opts )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2018-08-01 23:02:35 +00:00
} )
if err != nil {
2021-11-04 10:12:57 +00:00
return fmt . Errorf ( "couldn't purge directory: %w" , err )
2018-08-01 23:02:35 +00:00
}
return nil
}
// Rmdir deletes the root folder
//
// Returns an error if it isn't empty
2019-06-17 08:34:30 +00:00
func ( f * Fs ) Rmdir ( ctx context . Context , dir string ) error {
return f . purgeCheck ( ctx , dir , true )
2018-08-01 23:02:35 +00:00
}
// Precision return the precision of this Fs
func ( f * Fs ) Precision ( ) time . Duration {
return time . Second
}
// Purge deletes all the files and the container
2020-06-04 21:25:14 +00:00
func ( f * Fs ) Purge ( ctx context . Context , dir string ) error {
return f . purgeCheck ( ctx , dir , false )
2018-08-01 23:02:35 +00:00
}
2022-04-22 13:11:21 +00:00
// createOrUpdate tries to make remote file match without uploading.
// If the remote file exists, and has matching size and md5, only
// timestamps are updated. If the file does not exist or does does
// not match size and md5, but matching content can be constructed
// from deduplication, the file will be updated/created. If the file
// is currently in trash, but can be made to match, it will be
// restored. Returns ErrorObjectNotFound if upload will be necessary
// to get a matching remote file.
2022-08-04 18:48:13 +00:00
func ( f * Fs ) createOrUpdate ( ctx context . Context , file string , createTime time . Time , modTime time . Time , size int64 , md5 string ) ( info * api . JottaFile , err error ) {
2022-04-22 13:11:21 +00:00
opts := rest . Opts {
Method : "POST" ,
Path : f . filePath ( file ) ,
Parameters : url . Values { } ,
ExtraHeaders : make ( map [ string ] string ) ,
}
opts . Parameters . Set ( "cphash" , "true" )
opts . ExtraHeaders [ "JSize" ] = strconv . FormatInt ( size , 10 )
opts . ExtraHeaders [ "JMd5" ] = md5
2022-08-04 18:48:13 +00:00
opts . ExtraHeaders [ "JCreated" ] = api . JottaTime ( createTime ) . String ( )
opts . ExtraHeaders [ "JModified" ] = api . JottaTime ( modTime ) . String ( )
2022-04-22 13:11:21 +00:00
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err = f . jfsSrv . CallXML ( ctx , & opts , nil , & info )
2022-04-22 13:11:21 +00:00
return shouldRetry ( ctx , resp , err )
} )
if apiErr , ok := err . ( * api . Error ) ; ok {
// does not exist, i.e. not matching size and md5, and not possible to make it by deduplication
if apiErr . StatusCode == http . StatusNotFound {
return nil , fs . ErrorObjectNotFound
}
}
return info , nil
}
2019-02-07 17:41:17 +00:00
// copyOrMoves copies or moves directories or files depending on the method parameter
2019-09-04 19:00:37 +00:00
func ( f * Fs ) copyOrMove ( ctx context . Context , method , src , dest string ) ( info * api . JottaFile , err error ) {
2018-08-01 23:02:35 +00:00
opts := rest . Opts {
Method : "POST" ,
Path : src ,
Parameters : url . Values { } ,
}
2022-01-18 20:48:44 +00:00
opts . Parameters . Set ( method , f . filePathRaw ( dest , true ) )
2018-08-01 23:02:35 +00:00
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err = f . jfsSrv . CallXML ( ctx , & opts , nil , & info )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2018-08-01 23:02:35 +00:00
} )
if err != nil {
return nil , err
}
return info , nil
}
2020-10-13 21:43:40 +00:00
// Copy src to this remote using server-side copy operations.
2018-08-01 23:02:35 +00:00
//
2022-08-05 15:35:41 +00:00
// This is stored with the remote path given.
2018-08-01 23:02:35 +00:00
//
2022-08-05 15:35:41 +00:00
// It returns the destination Object and a possible error.
2018-08-01 23:02:35 +00:00
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantCopy
2019-06-17 08:34:30 +00:00
func ( f * Fs ) Copy ( ctx context . Context , src fs . Object , remote string ) ( fs . Object , error ) {
2018-08-01 23:02:35 +00:00
srcObj , ok := src . ( * Object )
if ! ok {
fs . Debugf ( src , "Can't copy - not same remote type" )
return nil , fs . ErrorCantMove
}
2019-06-17 08:34:30 +00:00
err := f . mkParentDir ( ctx , remote )
2018-08-01 23:02:35 +00:00
if err != nil {
return nil , err
}
2019-09-04 19:00:37 +00:00
info , err := f . copyOrMove ( ctx , "cp" , srcObj . filePath ( ) , remote )
2018-08-01 23:02:35 +00:00
2022-08-14 02:56:32 +00:00
// if destination was a trashed file then after a successful copy the copied file is still in trash (bug in api?)
2022-04-22 13:11:21 +00:00
if err == nil && bool ( info . Deleted ) && ! f . opt . TrashedOnly && info . State == "COMPLETED" {
fs . Debugf ( src , "Server-side copied to trashed destination, restoring" )
2022-08-04 18:48:13 +00:00
info , err = f . createOrUpdate ( ctx , remote , srcObj . createTime , srcObj . modTime , srcObj . size , srcObj . md5 )
2022-04-22 13:11:21 +00:00
}
2018-08-01 23:02:35 +00:00
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "couldn't copy file: %w" , err )
2018-08-01 23:02:35 +00:00
}
2019-09-04 19:00:37 +00:00
return f . newObjectWithInfo ( ctx , remote , info )
2018-08-01 23:02:35 +00:00
//return f.newObjectWithInfo(remote, &result)
}
2020-10-13 21:43:40 +00:00
// Move src to this remote using server-side move operations.
2018-08-01 23:02:35 +00:00
//
2022-08-05 15:35:41 +00:00
// This is stored with the remote path given.
2018-08-01 23:02:35 +00:00
//
2022-08-05 15:35:41 +00:00
// It returns the destination Object and a possible error.
2018-08-01 23:02:35 +00:00
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantMove
2019-06-17 08:34:30 +00:00
func ( f * Fs ) Move ( ctx context . Context , src fs . Object , remote string ) ( fs . Object , error ) {
2018-08-01 23:02:35 +00:00
srcObj , ok := src . ( * Object )
if ! ok {
fs . Debugf ( src , "Can't move - not same remote type" )
return nil , fs . ErrorCantMove
}
2019-06-17 08:34:30 +00:00
err := f . mkParentDir ( ctx , remote )
2018-08-01 23:02:35 +00:00
if err != nil {
return nil , err
}
2019-09-04 19:00:37 +00:00
info , err := f . copyOrMove ( ctx , "mv" , srcObj . filePath ( ) , remote )
2018-08-01 23:02:35 +00:00
if err != nil {
2021-11-04 10:12:57 +00:00
return nil , fmt . Errorf ( "couldn't move file: %w" , err )
2018-08-01 23:02:35 +00:00
}
2019-09-04 19:00:37 +00:00
return f . newObjectWithInfo ( ctx , remote , info )
2018-08-01 23:02:35 +00:00
//return f.newObjectWithInfo(remote, result)
}
// DirMove moves src, srcRemote to this remote at dstRemote
2020-10-13 21:43:40 +00:00
// using server-side move operations.
2018-08-01 23:02:35 +00:00
//
// 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
2019-06-17 08:34:30 +00:00
func ( f * Fs ) DirMove ( ctx context . Context , src fs . Fs , srcRemote , dstRemote string ) error {
2018-08-01 23:02:35 +00:00
srcFs , ok := src . ( * Fs )
if ! ok {
fs . Debugf ( srcFs , "Can't move directory - not same remote type" )
return fs . ErrorCantDirMove
}
srcPath := path . Join ( srcFs . root , srcRemote )
dstPath := path . Join ( f . root , dstRemote )
// Refuse to move to or from the root
if srcPath == "" || dstPath == "" {
fs . Debugf ( src , "DirMove error: Can't move root" )
return errors . New ( "can't move root directory" )
}
//fmt.Printf("Move src: %s (FullPath %s), dst: %s (FullPath: %s)\n", srcRemote, srcPath, dstRemote, dstPath)
var err error
2019-06-17 08:34:30 +00:00
_ , err = f . List ( ctx , dstRemote )
2018-08-01 23:02:35 +00:00
if err == fs . ErrorDirNotFound {
// OK
} else if err != nil {
return err
} else {
return fs . ErrorDirExists
}
2022-01-18 20:48:44 +00:00
_ , err = f . copyOrMove ( ctx , "mvDir" , path . Join ( f . fileEndpoint , f . opt . Enc . FromStandardPath ( srcPath ) ) + "/" , dstRemote )
2018-08-01 23:02:35 +00:00
if err != nil {
2021-11-04 10:12:57 +00:00
return fmt . Errorf ( "couldn't move directory: %w" , err )
2018-08-01 23:02:35 +00:00
}
return nil
}
2018-09-04 19:02:35 +00:00
// PublicLink generates a public link to the remote path (usually readable by anyone)
2020-05-31 21:18:01 +00:00
func ( f * Fs ) PublicLink ( ctx context . Context , remote string , expire fs . Duration , unlink bool ) ( link string , err error ) {
2018-09-04 19:02:35 +00:00
opts := rest . Opts {
Method : "GET" ,
Path : f . filePath ( remote ) ,
Parameters : url . Values { } ,
}
2020-05-31 21:18:01 +00:00
if unlink {
2018-09-04 19:02:35 +00:00
opts . Parameters . Set ( "mode" , "disableShare" )
} else {
opts . Parameters . Set ( "mode" , "enableShare" )
}
var resp * http . Response
var result api . JottaFile
err = f . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err = f . jfsSrv . CallXML ( ctx , & opts , nil , & result )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2018-09-04 19:02:35 +00:00
} )
if apiErr , ok := err . ( * api . Error ) ; ok {
// does not exist
if apiErr . StatusCode == http . StatusNotFound {
return "" , fs . ErrorObjectNotFound
}
}
if err != nil {
2020-05-31 21:18:01 +00:00
if unlink {
2021-11-04 10:12:57 +00:00
return "" , fmt . Errorf ( "couldn't remove public link: %w" , err )
2018-09-04 19:02:35 +00:00
}
2021-11-04 10:12:57 +00:00
return "" , fmt . Errorf ( "couldn't create public link: %w" , err )
2018-09-04 19:02:35 +00:00
}
2020-05-31 21:18:01 +00:00
if unlink {
2021-06-10 17:44:11 +00:00
if result . PublicURI != "" {
2021-11-04 10:12:57 +00:00
return "" , fmt . Errorf ( "couldn't remove public link - %q" , result . PublicURI )
2018-09-04 19:02:35 +00:00
}
return "" , nil
}
2021-06-10 17:44:11 +00:00
if result . PublicURI == "" {
return "" , errors . New ( "couldn't create public link - no uri received" )
2018-09-04 19:02:35 +00:00
}
2021-06-10 17:44:11 +00:00
if result . PublicSharePath != "" {
2022-01-18 20:06:22 +00:00
webLink := joinPath ( wwwURL , result . PublicSharePath )
2021-06-10 17:44:11 +00:00
fs . Debugf ( nil , "Web link: %s" , webLink )
} else {
fs . Debugf ( nil , "No web link received" )
}
2022-01-18 20:06:22 +00:00
directLink := joinPath ( wwwURL , fmt . Sprintf ( "opin/io/downloadPublic/%s/%s" , f . user , result . PublicURI ) )
2021-06-10 17:44:11 +00:00
fs . Debugf ( nil , "Direct link: %s" , directLink )
return directLink , nil
2018-09-04 19:02:35 +00:00
}
2018-08-14 23:12:20 +00:00
// About gets quota information
2019-06-17 08:34:30 +00:00
func ( f * Fs ) About ( ctx context . Context ) ( * fs . Usage , error ) {
2022-01-18 20:06:22 +00:00
info , err := getDriveInfo ( ctx , f . jfsSrv , f . user )
2018-08-14 23:12:20 +00:00
if err != nil {
return nil , err
}
usage := & fs . Usage {
2018-09-06 13:13:38 +00:00
Used : fs . NewUsageValue ( info . Usage ) ,
2018-08-14 23:12:20 +00:00
}
2018-09-06 17:55:16 +00:00
if info . Capacity > 0 {
usage . Total = fs . NewUsageValue ( info . Capacity )
usage . Free = fs . NewUsageValue ( info . Capacity - info . Usage )
}
2018-08-14 23:12:20 +00:00
return usage , nil
}
2021-06-10 14:45:13 +00:00
// UserInfo fetches info about the current user
func ( f * Fs ) UserInfo ( ctx context . Context ) ( userInfo map [ string ] string , err error ) {
cust , err := getCustomerInfo ( ctx , f . apiSrv )
if err != nil {
return nil , err
}
return map [ string ] string {
"Username" : cust . Username ,
"Email" : cust . Email ,
"Name" : cust . Name ,
"AccountType" : cust . AccountType ,
"SubscriptionType" : cust . SubscriptionType ,
} , nil
}
2020-04-11 10:30:35 +00:00
// CleanUp empties the trash
func ( f * Fs ) CleanUp ( ctx context . Context ) error {
opts := rest . Opts {
Method : "POST" ,
Path : "files/v1/purge_trash" ,
}
var info api . TrashResponse
_ , err := f . apiSrv . CallJSON ( ctx , & opts , nil , & info )
if err != nil {
2021-11-04 10:12:57 +00:00
return fmt . Errorf ( "couldn't empty trash: %w" , err )
2020-04-11 10:30:35 +00:00
}
return nil
}
2018-08-01 23:02:35 +00:00
// Hashes returns the supported hash sets.
func ( f * Fs ) Hashes ( ) hash . Set {
return hash . Set ( hash . MD5 )
}
// ---------------------------------------------
// 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
}
2020-05-20 10:39:20 +00:00
// filePath returns an escaped file path (f.root, remote)
2019-08-14 17:39:19 +00:00
func ( o * Object ) filePath ( ) string {
return o . fs . filePath ( o . remote )
}
2018-08-01 23:02:35 +00:00
// Hash returns the MD5 of an object returning a lowercase hex string
2019-06-17 08:34:30 +00:00
func ( o * Object ) Hash ( ctx context . Context , t hash . Type ) ( string , error ) {
2018-08-01 23:02:35 +00:00
if t != hash . MD5 {
return "" , hash . ErrUnsupported
}
return o . md5 , nil
}
// Size returns the size of an object in bytes
func ( o * Object ) Size ( ) int64 {
2019-09-04 19:00:37 +00:00
ctx := context . TODO ( )
err := o . readMetaData ( ctx , false )
2018-08-01 23:02:35 +00:00
if err != nil {
fs . Logf ( o , "Failed to read metadata: %v" , err )
return 0
}
return o . size
}
2018-08-14 22:33:58 +00:00
// MimeType of an Object if known, "" otherwise
2019-06-17 08:34:30 +00:00
func ( o * Object ) MimeType ( ctx context . Context ) string {
2018-08-14 22:33:58 +00:00
return o . mimeType
}
2021-09-29 13:34:59 +00:00
// validFile checks if info indicates file is valid
func ( f * Fs ) validFile ( info * api . JottaFile ) bool {
if info . State != "COMPLETED" {
return false // File is incomplete or corrupt
}
if ! info . Deleted {
return ! f . opt . TrashedOnly // Regular file; return false if TrashedOnly, else true
}
return f . opt . TrashedOnly // Deleted file; return true if TrashedOnly, else false
}
// validFolder checks if info indicates folder is valid
func ( f * Fs ) validFolder ( info * api . JottaFolder ) bool {
// Returns true if folder is not deleted.
// If TrashedOnly option then always returns true, because a folder not
// in trash must be traversed to get to files/subfolders that are.
return ! bool ( info . Deleted ) || f . opt . TrashedOnly
}
2018-08-01 23:02:35 +00:00
// setMetaData sets the metadata from info
func ( o * Object ) setMetaData ( info * api . JottaFile ) ( err error ) {
o . hasMetaData = true
2019-01-11 17:17:46 +00:00
o . size = info . Size
2018-08-01 23:02:35 +00:00
o . md5 = info . MD5
2018-08-14 22:33:58 +00:00
o . mimeType = info . MimeType
2022-08-04 18:48:13 +00:00
o . createTime = time . Time ( info . CreatedAt )
2018-08-01 23:02:35 +00:00
o . modTime = time . Time ( info . ModifiedAt )
2022-08-04 18:48:13 +00:00
o . updateTime = time . Time ( info . UpdatedAt )
2018-08-01 23:02:35 +00:00
return nil
}
2019-08-14 17:39:19 +00:00
// readMetaData reads and updates the metadata for an object
2019-09-04 19:00:37 +00:00
func ( o * Object ) readMetaData ( ctx context . Context , force bool ) ( err error ) {
2018-12-29 23:53:18 +00:00
if o . hasMetaData && ! force {
2018-08-01 23:02:35 +00:00
return nil
}
2019-09-04 19:00:37 +00:00
info , err := o . fs . readMetaDataForPath ( ctx , o . remote )
2018-08-01 23:02:35 +00:00
if err != nil {
return err
}
2021-09-29 13:34:59 +00:00
if ! o . fs . validFile ( info ) {
return fs . ErrorObjectNotFound
2018-12-29 23:53:18 +00:00
}
2018-08-01 23:02:35 +00:00
return o . setMetaData ( info )
}
// 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
2019-06-17 08:34:30 +00:00
func ( o * Object ) ModTime ( ctx context . Context ) time . Time {
2019-09-04 19:00:37 +00:00
err := o . readMetaData ( ctx , false )
2018-08-01 23:02:35 +00:00
if err != nil {
fs . Logf ( o , "Failed to read metadata: %v" , err )
return time . Now ( )
}
return o . modTime
}
// SetModTime sets the modification time of the local fs object
2019-06-17 08:34:30 +00:00
func ( o * Object ) SetModTime ( ctx context . Context , modTime time . Time ) error {
2021-09-26 21:01:00 +00:00
// make sure metadata is available, we need its current size and md5
err := o . readMetaData ( ctx , false )
if err != nil {
fs . Logf ( o , "Failed to read metadata: %v" , err )
return err
}
2022-04-22 13:19:51 +00:00
// request check/update with existing metadata and new modtime
// (note that if size/md5 does not match, the file content will
// also be modified if deduplication is possible, i.e. it is
// important to use correct/latest values)
2022-08-04 18:48:13 +00:00
_ , err = o . fs . createOrUpdate ( ctx , o . remote , o . createTime , modTime , o . size , o . md5 )
2021-09-26 21:01:00 +00:00
if err != nil {
2022-04-22 13:19:51 +00:00
if err == fs . ErrorObjectNotFound {
// file was modified (size/md5 changed) between readMetaData and createOrUpdate?
return errors . New ( "metadata did not match" )
}
2021-09-26 21:01:00 +00:00
return err
}
// update local metadata
o . modTime = modTime
return nil
2018-08-01 23:02:35 +00:00
}
// Storable returns a boolean showing whether this object storable
func ( o * Object ) Storable ( ) bool {
return true
}
// Open an object for read
2019-06-17 08:34:30 +00:00
func ( o * Object ) Open ( ctx context . Context , options ... fs . OpenOption ) ( in io . ReadCloser , err error ) {
2018-08-01 23:02:35 +00:00
fs . FixRangeOption ( options , o . size )
var resp * http . Response
opts := rest . Opts {
Method : "GET" ,
Path : o . filePath ( ) ,
Parameters : url . Values { } ,
Options : options ,
}
opts . Parameters . Set ( "mode" , "bin" )
err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err = o . fs . jfsSrv . Call ( ctx , & opts )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2018-08-01 23:02:35 +00:00
} )
if err != nil {
return nil , err
}
return resp . Body , err
}
2018-08-20 14:38:21 +00:00
// Read the md5 of in returning a reader which will read the same contents
//
// The cleanup function should be called when out is finished with
// regardless of whether this function returned an error or not.
func readMD5 ( in io . Reader , size , threshold int64 ) ( md5sum string , out io . Reader , cleanup func ( ) , err error ) {
2020-05-25 06:05:53 +00:00
// we need an MD5
2018-08-20 14:38:21 +00:00
md5Hasher := md5 . New ( )
2019-02-07 17:41:17 +00:00
// use the teeReader to write to the local file AND calculate the MD5 while doing so
2018-08-20 14:38:21 +00:00
teeReader := io . TeeReader ( in , md5Hasher )
// nothing to clean up by default
cleanup = func ( ) { }
// don't cache small files on disk to reduce wear of the disk
if size > threshold {
var tempFile * os . File
// create the cache file
2022-08-20 14:38:02 +00:00
tempFile , err = os . CreateTemp ( "" , cachePrefix )
2018-08-20 14:38:21 +00:00
if err != nil {
return
}
_ = os . Remove ( tempFile . Name ( ) ) // Delete the file - may not work on Windows
// clean up the file after we are done downloading
cleanup = func ( ) {
// the file should normally already be close, but just to make sure
_ = tempFile . Close ( )
_ = os . Remove ( tempFile . Name ( ) ) // delete the cache file after we are done - may be deleted already
}
// copy the ENTIRE file to disc and calculate the MD5 in the process
if _ , err = io . Copy ( tempFile , teeReader ) ; err != nil {
return
}
// jump to the start of the local file so we can pass it along
if _ , err = tempFile . Seek ( 0 , 0 ) ; err != nil {
return
}
// replace the already read source with a reader of our cached file
out = tempFile
} else {
// that's a small file, just read it into memory
var inData [ ] byte
2022-08-20 14:38:02 +00:00
inData , err = io . ReadAll ( teeReader )
2018-08-20 14:38:21 +00:00
if err != nil {
return
}
// set the reader to our read memory block
out = bytes . NewReader ( inData )
}
return hex . EncodeToString ( md5Hasher . Sum ( nil ) ) , out , cleanup , nil
}
2018-08-01 23:02:35 +00:00
// Update the object with the contents of the io.Reader, modTime and size
//
2022-08-05 15:35:41 +00:00
// If existing is set then it updates the object rather than creating a new one.
2018-08-01 23:02:35 +00:00
//
// The new object may have been created if an error is returned
2019-06-17 08:34:30 +00:00
func ( o * Object ) Update ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( err error ) {
2021-07-01 21:17:05 +00:00
if o . fs . opt . NoVersions {
err := o . readMetaData ( ctx , false )
if err == nil {
// if the object exists delete it
err = o . remove ( ctx , true )
2023-03-16 21:37:10 +00:00
if err != nil && err != fs . ErrorObjectNotFound {
// if delete failed then report that, unless it was because the file did not exist after all
2021-11-04 10:12:57 +00:00
return fmt . Errorf ( "failed to remove old object: %w" , err )
2021-07-01 21:17:05 +00:00
}
2023-03-16 21:37:10 +00:00
} else if err != fs . ErrorObjectNotFound {
// if the object does not exist we can just continue but if the error is something different we should report that
2021-07-01 21:17:05 +00:00
return err
}
}
2020-12-20 16:29:57 +00:00
o . fs . tokenRenewer . Start ( )
defer o . fs . tokenRenewer . Stop ( )
2018-08-20 14:38:21 +00:00
size := src . Size ( )
2019-06-17 08:34:30 +00:00
md5String , err := src . Hash ( ctx , hash . MD5 )
2018-08-14 21:15:21 +00:00
if err != nil || md5String == "" {
2018-08-20 14:38:21 +00:00
// unwrap the accounting from the input, we use wrap to put it
// back on after the buffering
var wrap accounting . WrapFn
in , wrap = accounting . UnWrap ( in )
var cleanup func ( )
md5String , in , cleanup , err = readMD5 ( in , size , int64 ( o . fs . opt . MD5MemoryThreshold ) )
defer cleanup ( )
if err != nil {
2021-11-04 10:12:57 +00:00
return fmt . Errorf ( "failed to calculate MD5: %w" , err )
2018-08-14 21:15:21 +00:00
}
2018-08-20 14:38:21 +00:00
// Wrap the accounting back onto the stream
in = wrap ( in )
2018-08-14 21:15:21 +00:00
}
2022-08-04 18:48:13 +00:00
// Fetch metadata if --metadata is in use
meta , err := fs . GetMetadataOptions ( ctx , src , options )
if err != nil {
return fmt . Errorf ( "failed to read metadata from source object: %w" , err )
}
var createdTime string
var modTime string
if meta != nil {
if v , ok := meta [ "btime" ] ; ok {
t , err := time . Parse ( time . RFC3339Nano , v ) // metadata stores RFC3339Nano timestamps
if err != nil {
fs . Debugf ( o , "failed to parse metadata btime: %q: %v" , v , err )
} else {
createdTime = api . Rfc3339Time ( t ) . String ( ) // jottacloud api wants RFC3339 timestamps
}
}
if v , ok := meta [ "mtime" ] ; ok {
t , err := time . Parse ( time . RFC3339Nano , v )
if err != nil {
fs . Debugf ( o , "failed to parse metadata mtime: %q: %v" , v , err )
} else {
modTime = api . Rfc3339Time ( t ) . String ( )
}
}
}
if modTime == "" { // prefer mtime in meta as Modified time, fallback to source ModTime
modTime = api . Rfc3339Time ( src . ModTime ( ctx ) ) . String ( )
}
if createdTime == "" { // if no Created time set same as Modified
createdTime = modTime
}
2018-08-14 21:15:21 +00:00
2018-08-22 20:26:18 +00:00
// use the api to allocate the file first and get resume / deduplication info
2018-12-29 23:53:18 +00:00
var resp * http . Response
2018-08-01 23:02:35 +00:00
opts := rest . Opts {
2018-08-22 20:26:18 +00:00
Method : "POST" ,
2019-08-13 13:28:46 +00:00
Path : "files/v1/allocate" ,
2020-03-21 21:54:00 +00:00
Options : options ,
2018-08-22 20:26:18 +00:00
ExtraHeaders : make ( map [ string ] string ) ,
}
2018-08-01 23:02:35 +00:00
2018-08-22 20:26:18 +00:00
// the allocate request
var request = api . AllocateFileRequest {
Bytes : size ,
2022-08-04 18:48:13 +00:00
Created : createdTime ,
Modified : modTime ,
2018-08-22 20:26:18 +00:00
Md5 : md5String ,
2022-01-18 20:48:44 +00:00
Path : o . fs . allocatePathRaw ( o . remote , true ) ,
2018-08-22 20:26:18 +00:00
}
// send it
var response api . AllocateFileResponse
2018-08-01 23:02:35 +00:00
err = o . fs . pacer . CallNoRetry ( func ( ) ( bool , error ) {
2019-09-04 19:00:37 +00:00
resp , err = o . fs . apiSrv . CallJSON ( ctx , & opts , & request , & response )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2018-08-01 23:02:35 +00:00
} )
if err != nil {
return err
}
2022-08-04 18:48:13 +00:00
// If the file state is INCOMPLETE and CORRUPT, we must upload it.
// Else, if the file state is COMPLETE, we don't need to upload it because
// the content is already there, possibly it was created with deduplication,
// and also any metadata changes are already performed by the allocate request.
2018-08-22 20:26:18 +00:00
if response . State != "COMPLETED" {
// how much do we still have to upload?
remainingBytes := size - response . ResumePos
opts = rest . Opts {
Method : "POST" ,
RootURL : response . UploadURL ,
ContentLength : & remainingBytes ,
2018-12-29 23:53:18 +00:00
ContentType : "application/octet-stream" ,
2018-08-22 20:26:18 +00:00
Body : in ,
ExtraHeaders : make ( map [ string ] string ) ,
}
if response . ResumePos != 0 {
opts . ExtraHeaders [ "Range" ] = "bytes=" + strconv . FormatInt ( response . ResumePos , 10 ) + "-" + strconv . FormatInt ( size - 1 , 10 )
}
// copy the already uploaded bytes into the trash :)
var result api . UploadResponse
2022-08-20 14:38:02 +00:00
_ , err = io . CopyN ( io . Discard , in , response . ResumePos )
2018-08-22 20:26:18 +00:00
if err != nil {
return err
}
// send the remaining bytes
2022-08-04 18:48:13 +00:00
_ , err = o . fs . apiSrv . CallJSON ( ctx , & opts , nil , & result )
2018-08-22 20:26:18 +00:00
if err != nil {
return err
}
2022-08-04 18:48:13 +00:00
// Upload response contains main metadata properties (size, md5 and modTime)
// which could be set back to the object, but it does not contain the
// necessary information to set the createTime and updateTime properties,
// so must therefore perform a read instead.
2018-08-22 20:26:18 +00:00
}
2022-08-04 18:48:13 +00:00
// in any case we must update the object meta data
return o . readMetaData ( ctx , true )
2018-08-01 23:02:35 +00:00
}
2021-07-01 21:17:05 +00:00
func ( o * Object ) remove ( ctx context . Context , hard bool ) error {
2018-08-01 23:02:35 +00:00
opts := rest . Opts {
Method : "POST" ,
Path : o . filePath ( ) ,
Parameters : url . Values { } ,
2018-10-13 11:32:46 +00:00
NoResponse : true ,
2018-08-01 23:02:35 +00:00
}
2021-07-01 21:17:05 +00:00
if hard {
2018-09-07 11:58:18 +00:00
opts . Parameters . Set ( "rm" , "true" )
} else {
opts . Parameters . Set ( "dl" , "true" )
}
2018-08-01 23:02:35 +00:00
2023-03-16 21:37:10 +00:00
err := o . fs . pacer . Call ( func ( ) ( bool , error ) {
2022-01-18 20:06:22 +00:00
resp , err := o . fs . jfsSrv . CallXML ( ctx , & opts , nil , nil )
2021-03-11 14:44:01 +00:00
return shouldRetry ( ctx , resp , err )
2018-08-01 23:02:35 +00:00
} )
2023-03-16 21:37:10 +00:00
if apiErr , ok := err . ( * api . Error ) ; ok {
// attempting to hard delete will fail if path does not exist, but standard delete will succeed
if apiErr . StatusCode == http . StatusNotFound {
return fs . ErrorObjectNotFound
}
}
return err
2018-08-01 23:02:35 +00:00
}
2021-07-01 21:17:05 +00:00
// Remove an object
func ( o * Object ) Remove ( ctx context . Context ) error {
return o . remove ( ctx , o . fs . opt . HardDelete )
}
2022-08-04 18:48:13 +00:00
// Metadata returns metadata for an object
//
// It should return nil if there is no Metadata
func ( o * Object ) Metadata ( ctx context . Context ) ( metadata fs . Metadata , err error ) {
err = o . readMetaData ( ctx , false )
if err != nil {
fs . Logf ( o , "Failed to read metadata: %v" , err )
return nil , err
}
metadata . Set ( "btime" , o . createTime . Format ( time . RFC3339Nano ) ) // metadata timestamps should be RFC3339Nano
metadata . Set ( "mtime" , o . modTime . Format ( time . RFC3339Nano ) )
metadata . Set ( "utime" , o . updateTime . Format ( time . RFC3339Nano ) )
metadata . Set ( "content-type" , o . mimeType )
return metadata , nil
}
2018-08-01 23:02:35 +00:00
// Check the interfaces are satisfied
var (
2018-09-04 19:02:35 +00:00
_ fs . Fs = ( * Fs ) ( nil )
_ fs . Purger = ( * Fs ) ( nil )
_ fs . Copier = ( * Fs ) ( nil )
_ fs . Mover = ( * Fs ) ( nil )
_ fs . DirMover = ( * Fs ) ( nil )
_ fs . ListRer = ( * Fs ) ( nil )
_ fs . PublicLinker = ( * Fs ) ( nil )
_ fs . Abouter = ( * Fs ) ( nil )
2021-06-10 14:45:13 +00:00
_ fs . UserInfoer = ( * Fs ) ( nil )
2020-04-11 10:30:35 +00:00
_ fs . CleanUpper = ( * Fs ) ( nil )
2018-09-04 19:02:35 +00:00
_ fs . Object = ( * Object ) ( nil )
_ fs . MimeTyper = ( * Object ) ( nil )
2022-08-04 18:48:13 +00:00
_ fs . Metadataer = ( * Object ) ( nil )
2018-08-01 23:02:35 +00:00
)