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"
2018-08-01 23:02:35 +00:00
"fmt"
"io"
2018-08-14 21:15:21 +00:00
"io/ioutil"
2019-01-28 20:50:51 +00:00
"log"
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"
"github.com/pkg/errors"
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"
2020-06-10 19:49:29 +00:00
rootURL = "https://jfs.jottacloud.com/jfs/"
2020-06-11 11:02:28 +00:00
apiURL = "https://api.jottacloud.com/"
baseURL = "https://www.jottacloud.com/"
defaultTokenURL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token"
cachePrefix = "rclone-jcmd5-"
configDevice = "device"
configMountpoint = "mountpoint"
configTokenURL = "tokenURL"
configClientID = "client_id"
configClientSecret = "client_secret"
configVersion = 1
v1tokenURL = "https://api.jottacloud.com/auth/v1/token"
v1registerURL = "https://api.jottacloud.com/auth/v1/register"
v1ClientID = "nibfk8biu12ju7hpqomr8b1e40"
v1EncryptedClientSecret = "Vp8eAv7eVElMnQwN-kgU9cbhgApNDaMqWdlDi5qFydlQoji4JBxrGMF2"
v1configVersion = 0
2018-08-22 20:26:18 +00:00
)
var (
// Description of how to auth for this app for a personal account
oauthConfig = & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
2019-12-28 16:45:04 +00:00
AuthURL : defaultTokenURL ,
TokenURL : defaultTokenURL ,
2018-08-22 20:26:18 +00:00
} ,
2019-04-07 14:12:32 +00:00
RedirectURL : oauthutil . RedirectLocalhostURL ,
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 ,
2018-08-22 20:26:18 +00:00
Config : func ( name string , m configmap . Mapper ) {
2019-09-04 19:00:37 +00:00
ctx := context . TODO ( )
2018-12-30 19:22:33 +00:00
2019-11-19 23:10:38 +00:00
refresh := false
if version , ok := m . Get ( "configVersion" ) ; ok {
ver , err := strconv . Atoi ( version )
2019-04-07 14:12:32 +00:00
if err != nil {
2019-11-19 23:10:38 +00:00
log . Fatalf ( "Failed to parse config version - corrupted config" )
2019-04-07 14:12:32 +00:00
}
2020-06-11 11:02:28 +00:00
refresh = ( ver != configVersion ) && ( ver != v1configVersion )
2019-04-07 14:12:32 +00:00
}
2019-11-19 23:10:38 +00:00
if refresh {
fmt . Printf ( "Config outdated - refreshing\n" )
} else {
tokenString , ok := m . Get ( "token" )
if ok && tokenString != "" {
fmt . Printf ( "Already have a token - refresh?\n" )
if ! config . Confirm ( false ) {
return
}
}
2019-04-07 14:12:32 +00:00
}
2020-06-11 11:02:28 +00:00
fmt . Printf ( "Use legacy authentification?.\nThis is only required for certain whitelabel versions of Jottacloud and not recommended for normal users.\n" )
2019-11-05 11:53:44 +00:00
if config . Confirm ( false ) {
2020-06-11 11:02:28 +00:00
v1config ( ctx , name , m )
} else {
v2config ( ctx , name , m )
2018-08-22 20:26:18 +00:00
}
} ,
2018-08-01 23:02:35 +00:00
Options : [ ] 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" ,
Help : "Only show files that are in the trash.\nThis will show trashed files in their original directory structure." ,
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 ,
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 ) ,
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" `
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 {
2018-08-22 20:26:18 +00:00
name string
root string
user string
opt Options
features * fs . Features
endpointURL string
srv * rest . Client
apiSrv * rest . Client
2019-02-09 20:52:15 +00:00
pacer * fs . Pacer
2018-08-22 20:26:18 +00:00
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
modTime time . Time
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
}
2020-05-20 10:39:20 +00:00
// parsePath parses a box 'url'
2018-08-01 23:02:35 +00:00
func parsePath ( path string ) ( root string ) {
root = strings . Trim ( path , "/" )
return
}
// retryErrorCodes is a slice of error codes that we will retry
var retryErrorCodes = [ ] int {
429 , // Too Many Requests.
500 , // Internal Server Error
502 , // Bad Gateway
503 , // Service Unavailable
504 , // Gateway Timeout
509 , // Bandwidth Limit Exceeded
}
// shouldRetry returns a boolean as to whether this resp and err
// deserve to be retried. It returns the err as a convenience
func shouldRetry ( resp * http . Response , err error ) ( bool , error ) {
return fserrors . ShouldRetry ( err ) || fserrors . ShouldRetryHTTP ( resp , retryErrorCodes ) , err
}
2020-06-11 11:02:28 +00:00
// v1config configure a jottacloud backend using legacy authentification
func v1config ( ctx context . Context , name string , m configmap . Mapper ) {
srv := rest . NewClient ( fshttp . NewClient ( fs . Config ) )
fmt . Printf ( "\nDo you want to create a machine specific API key?\n\nRclone 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.\n\n" )
if config . Confirm ( false ) {
deviceRegistration , err := registerDevice ( ctx , srv )
if err != nil {
log . Fatalf ( "Failed to register device: %v" , err )
}
m . Set ( configClientID , deviceRegistration . ClientID )
m . Set ( configClientSecret , obscure . MustObscure ( deviceRegistration . ClientSecret ) )
fs . Debugf ( nil , "Got clientID '%s' and clientSecret '%s'" , deviceRegistration . ClientID , deviceRegistration . ClientSecret )
}
clientID , ok := m . Get ( configClientID )
if ! ok {
clientID = v1ClientID
}
clientSecret , ok := m . Get ( configClientSecret )
if ! ok {
clientSecret = v1EncryptedClientSecret
}
oauthConfig . ClientID = clientID
oauthConfig . ClientSecret = obscure . MustReveal ( clientSecret )
oauthConfig . Endpoint . AuthURL = v1tokenURL
oauthConfig . Endpoint . TokenURL = v1tokenURL
fmt . Printf ( "Username> " )
username := config . ReadLine ( )
password := config . GetPassword ( "Your Jottacloud password is only required during setup and will not be stored." )
token , err := doAuthV1 ( ctx , srv , username , password )
if err != nil {
log . Fatalf ( "Failed to get oauth token: %s" , err )
}
err = oauthutil . PutToken ( name , m , & token , true )
if err != nil {
log . Fatalf ( "Error while saving token: %s" , err )
}
fmt . Printf ( "\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n" )
if config . Confirm ( false ) {
oAuthClient , _ , err := oauthutil . NewClient ( name , m , oauthConfig )
if err != nil {
log . Fatalf ( "Failed to load oAuthClient: %s" , err )
}
srv = rest . NewClient ( oAuthClient ) . SetRoot ( rootURL )
apiSrv := rest . NewClient ( oAuthClient ) . SetRoot ( apiURL )
device , mountpoint , err := setupMountpoint ( ctx , srv , apiSrv )
if err != nil {
log . Fatalf ( "Failed to setup mountpoint: %s" , err )
}
m . Set ( configDevice , device )
m . Set ( configMountpoint , mountpoint )
}
m . Set ( "configVersion" , strconv . Itoa ( v1configVersion ) )
}
// 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" ,
RootURL : v1registerURL ,
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
}
// doAuthV1 runs the actual token request for V1 authentification
func doAuthV1 ( ctx context . Context , srv * rest . Client , username , password string ) ( token oauth2 . Token , err error ) {
// 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 ,
}
// do the first request
var jsonToken api . TokenJSON
resp , err := srv . CallJSON ( ctx , & opts , nil , & jsonToken )
if err != nil {
// 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" {
fmt . Printf ( "This account uses 2 factor authentication you will receive a verification code via SMS.\n" )
fmt . Printf ( "Enter verification code> " )
authCode := config . ReadLine ( )
authCode = strings . Replace ( authCode , "-" , "" , - 1 ) // remove any "-" contained in the code so we have a 6 digit number
opts . ExtraHeaders = make ( map [ string ] string )
opts . ExtraHeaders [ "X-Jottacloud-Otp" ] = authCode
resp , err = srv . CallJSON ( ctx , & opts , nil , & jsonToken )
}
}
}
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
}
// v2config configure a jottacloud backend using the modern JottaCli token based authentification
func v2config ( ctx context . Context , name string , m configmap . Mapper ) {
srv := rest . NewClient ( fshttp . NewClient ( fs . Config ) )
fmt . Printf ( "Generate a personal login token here: https://www.jottacloud.com/web/secure\n" )
fmt . Printf ( "Login Token> " )
loginToken := config . ReadLine ( )
token , err := doAuthV2 ( ctx , srv , loginToken , m )
if err != nil {
log . Fatalf ( "Failed to get oauth token: %s" , err )
}
err = oauthutil . PutToken ( name , m , & token , true )
if err != nil {
log . Fatalf ( "Error while saving token: %s" , err )
}
fmt . Printf ( "\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n" )
if config . Confirm ( false ) {
oauthConfig . ClientID = "jottacli"
oAuthClient , _ , err := oauthutil . NewClient ( name , m , oauthConfig )
if err != nil {
log . Fatalf ( "Failed to load oAuthClient: %s" , err )
}
srv = rest . NewClient ( oAuthClient ) . SetRoot ( rootURL )
apiSrv := rest . NewClient ( oAuthClient ) . SetRoot ( apiURL )
device , mountpoint , err := setupMountpoint ( ctx , srv , apiSrv )
if err != nil {
log . Fatalf ( "Failed to setup mountpoint: %s" , err )
}
m . Set ( configDevice , device )
m . Set ( configMountpoint , mountpoint )
}
m . Set ( "configVersion" , strconv . Itoa ( configVersion ) )
}
// doAuthV2 runs the actual token request for V2 authentification
func doAuthV2 ( ctx context . Context , srv * rest . Client , loginTokenBase64 string , m configmap . Mapper ) ( token oauth2 . Token , 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 {
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 {
return token , err
}
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
_ , err = srv . CallJSON ( ctx , & opts , nil , & wellKnown )
2019-11-19 23:10:38 +00:00
if err != nil {
return token , err
2018-08-01 23:02:35 +00:00
}
2019-12-28 16:45:04 +00:00
// save the tokenurl
oauthConfig . Endpoint . AuthURL = wellKnown . TokenEndpoint
oauthConfig . Endpoint . TokenURL = wellKnown . TokenEndpoint
m . Set ( configTokenURL , wellKnown . TokenEndpoint )
2019-08-14 17:39:19 +00:00
// prepare out token request with username and password
values := url . Values { }
2019-11-19 23:10:38 +00:00
values . Set ( "client_id" , "jottacli" )
values . Set ( "grant_type" , "password" )
values . Set ( "password" , loginToken . AuthToken )
values . Set ( "scope" , "offline_access+openid" )
values . Set ( "username" , loginToken . Username )
values . Encode ( )
opts = rest . Opts {
2019-08-14 17:39:19 +00:00
Method : "POST" ,
RootURL : oauthConfig . Endpoint . AuthURL ,
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
2019-11-19 23:10:38 +00:00
_ , err = srv . CallJSON ( ctx , & opts , nil , & jsonToken )
2019-08-14 17:39:19 +00:00
if err != nil {
2019-11-19 23:10:38 +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 )
return token , err
}
// setupMountpoint sets up a custom device and mountpoint if desired by the user
2019-09-04 19:00:37 +00:00
func setupMountpoint ( ctx context . Context , srv * rest . Client , apiSrv * rest . Client ) ( device , mountpoint string , err error ) {
cust , err := getCustomerInfo ( ctx , apiSrv )
2019-08-14 17:39:19 +00:00
if err != nil {
return "" , "" , err
}
2019-09-04 19:00:37 +00:00
acc , err := getDriveInfo ( ctx , srv , cust . Username )
2018-08-01 23:02:35 +00:00
if err != nil {
2019-08-14 17:39:19 +00:00
return "" , "" , err
2018-08-01 23:02:35 +00:00
}
2019-08-14 17:39:19 +00:00
var deviceNames [ ] string
for i := range acc . Devices {
deviceNames = append ( deviceNames , acc . Devices [ i ] . Name )
2018-08-01 23:02:35 +00:00
}
2019-08-14 17:39:19 +00:00
fmt . Printf ( "Please select the device to use. Normally this will be Jotta\n" )
device = config . Choose ( "Devices" , deviceNames , nil , false )
2019-09-04 19:00:37 +00:00
dev , err := getDeviceInfo ( ctx , srv , path . Join ( cust . Username , device ) )
2019-08-14 17:39:19 +00:00
if err != nil {
return "" , "" , err
}
if len ( dev . MountPoints ) == 0 {
return "" , "" , errors . New ( "no mountpoints for selected device" )
}
var mountpointNames [ ] string
for i := range dev . MountPoints {
mountpointNames = append ( mountpointNames , dev . MountPoints [ i ] . Name )
}
fmt . Printf ( "Please select the mountpoint to user. Normally this will be Archive\n" )
mountpoint = config . Choose ( "Mountpoints" , mountpointNames , nil , false )
return device , mountpoint , err
2018-08-01 23:02:35 +00:00
}
2019-08-14 17:39:19 +00:00
// getCustomerInfo queries general information about the account
2019-09-04 19:00:37 +00:00
func getCustomerInfo ( ctx context . Context , srv * rest . Client ) ( info * api . CustomerInfo , err error ) {
2019-08-13 13:28:46 +00:00
opts := rest . Opts {
Method : "GET" ,
Path : "account/v1/customer" ,
}
2019-09-04 19:00:37 +00:00
_ , err = srv . CallJSON ( ctx , & opts , nil , & info )
2019-08-13 13:28:46 +00:00
if err != nil {
2019-08-14 17:39:19 +00:00
return nil , errors . Wrap ( err , "couldn't get customer info" )
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 {
2019-08-14 17:39:19 +00:00
return nil , errors . Wrap ( err , "couldn't get drive info" )
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 {
2019-08-14 17:39:19 +00:00
return nil , errors . Wrap ( err , "couldn't get device info" )
2018-08-01 23:02:35 +00:00
}
2018-08-14 23:12:20 +00:00
return info , nil
}
2019-08-14 17:39:19 +00:00
// setEndpointURL generates the API endpoint URL
2019-08-13 13:28:46 +00:00
func ( f * Fs ) setEndpointURL ( ) {
2019-05-17 00:31:59 +00:00
if f . opt . Device == "" {
f . opt . Device = defaultDevice
}
if f . opt . Mountpoint == "" {
f . opt . Mountpoint = defaultMountpoint
}
2019-08-13 13:28:46 +00:00
f . endpointURL = urlPathEscape ( path . Join ( f . user , 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 ) {
2019-09-04 19:00:37 +00:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & result )
2019-08-14 17:39:19 +00:00
return shouldRetry ( resp , err )
} )
if apiErr , ok := err . ( * api . Error ) ; ok {
// does not exist
if apiErr . StatusCode == http . StatusNotFound {
return nil , fs . ErrorObjectNotFound
}
}
if err != nil {
return nil , errors . Wrap ( err , "read metadata failed" )
}
if result . XMLName . Local != "file" {
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 {
return strings . Replace ( rest . URLPathEscape ( in ) , "+" , "%2B" , - 1 )
}
2018-09-06 13:13:38 +00:00
// filePathRaw returns an unescaped file path (f.root, file)
func ( f * Fs ) filePathRaw ( file string ) string {
2020-01-14 17:33:35 +00:00
return path . Join ( f . endpointURL , 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 {
2018-09-16 20:30:20 +00:00
return urlPathEscape ( f . filePathRaw ( file ) )
2018-08-01 23:02:35 +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 ) {
if v1tokenURL == req . URL . String ( ) {
fmt . Printf ( "grantFilter\n" )
// read the entire body
refreshBody , err := ioutil . ReadAll ( req . Body )
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())
req . Body = ioutil . NopCloser ( bytes . NewReader ( refreshBody ) )
}
}
2018-08-01 23:02:35 +00:00
// NewFs constructs an Fs from the path, container:path
func NewFs ( name , root string , m configmap . Mapper ) ( fs . Fs , error ) {
2019-09-04 19:00:37 +00:00
ctx := context . TODO ( )
2018-08-01 23:02:35 +00:00
// Parse config into Options struct
opt := new ( Options )
err := configstruct . Set ( m , opt )
if err != nil {
return nil , err
}
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 {
return nil , errors . New ( "Failed to parse config version" )
}
2020-06-11 11:02:28 +00:00
ok = ( ver == configVersion ) || ( ver == v1configVersion )
2019-04-07 14:12:32 +00:00
}
if ! ok {
2019-11-19 23:10:38 +00:00
return nil , errors . New ( "Outdated config - please reconfigure this backend" )
2019-04-07 14:12:32 +00:00
}
2020-06-11 11:02:28 +00:00
baseClient := fshttp . NewClient ( fs . Config )
if ver == configVersion {
oauthConfig . ClientID = "jottacli"
// 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
}
} else if ver == v1configVersion {
clientID , ok := m . Get ( configClientID )
if ! ok {
clientID = v1ClientID
}
clientSecret , ok := m . Get ( configClientSecret )
if ! ok {
clientSecret = v1EncryptedClientSecret
}
oauthConfig . ClientID = clientID
oauthConfig . ClientSecret = obscure . MustReveal ( clientSecret )
oauthConfig . Endpoint . TokenURL = v1tokenURL
oauthConfig . Endpoint . AuthURL = v1tokenURL
// 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
2018-08-22 20:26:18 +00:00
oAuthClient , ts , err := oauthutil . NewClientWithBaseClient ( name , m , oauthConfig , baseClient )
if err != nil {
return nil , errors . Wrap ( err , "Failed to configure Jottacloud oauth client" )
}
2019-12-28 16:45:04 +00:00
rootIsDir := strings . HasSuffix ( root , "/" )
root = parsePath ( root )
2018-08-01 23:02:35 +00:00
f := & Fs {
2019-01-28 20:50:51 +00:00
name : name ,
root : root ,
opt : * opt ,
2018-12-29 23:53:18 +00:00
srv : rest . NewClient ( oAuthClient ) . SetRoot ( rootURL ) ,
2018-08-22 20:26:18 +00:00
apiSrv : rest . NewClient ( oAuthClient ) . SetRoot ( apiURL ) ,
2019-02-09 20:52:15 +00:00
pacer : fs . NewPacer ( 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 ,
WriteMimeType : true ,
2018-08-01 23:02:35 +00:00
} ) . Fill ( f )
2018-08-22 20:26:18 +00:00
f . srv . 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 , "" )
2018-08-22 20:26:18 +00:00
return err
} )
2019-09-04 19:00:37 +00:00
cust , err := getCustomerInfo ( ctx , f . apiSrv )
2018-08-01 23:02:35 +00:00
if err != nil {
2019-08-14 17:39:19 +00:00
return nil , err
2018-08-01 23:02:35 +00:00
}
2019-08-13 13:28:46 +00:00
f . user = cust . Username
f . setEndpointURL ( )
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 {
if errors . Cause ( err ) == fs . ErrorObjectNotFound || errors . Cause ( err ) == fs . ErrorNotAFile {
// 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 {
// Set info
err = o . setMetaData ( info )
} 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 ) {
2019-09-04 19:00:37 +00:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & jf )
2018-08-01 23:02:35 +00:00
return shouldRetry ( resp , err )
} )
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 ) {
2019-09-04 19:00:37 +00:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & result )
2018-08-01 23:02:35 +00:00
return shouldRetry ( resp , err )
} )
if err != nil {
if apiErr , ok := err . ( * api . Error ) ; ok {
// does not exist
if apiErr . StatusCode == http . StatusNotFound {
return nil , fs . ErrorDirNotFound
}
}
return nil , errors . Wrap ( err , "couldn't list files" )
}
2020-04-11 10:11:20 +00:00
if bool ( result . Deleted ) && ! f . opt . TrashedOnly {
2018-08-01 23:02:35 +00:00
return nil , fs . ErrorDirNotFound
}
for i := range result . Folders {
item := & result . Folders [ i ]
2020-04-11 10:11:20 +00:00
if ! f . opt . TrashedOnly && bool ( item . Deleted ) {
2018-08-01 23:02:35 +00:00
continue
}
2020-01-14 17:33:35 +00:00
remote := path . Join ( dir , f . opt . Enc . ToStandardName ( item . Name ) )
2018-08-01 23:02:35 +00:00
d := fs . NewDir ( remote , time . Time ( item . ModifiedAt ) )
entries = append ( entries , d )
}
for i := range result . Files {
item := & result . Files [ i ]
2020-04-11 10:11:20 +00:00
if f . opt . TrashedOnly {
if ! item . Deleted || item . State != "COMPLETED" {
continue
}
} else {
if item . Deleted || item . State != "COMPLETED" {
continue
}
2018-08-01 23:02:35 +00:00
}
2020-01-14 17:33:35 +00:00
remote := path . Join ( dir , f . opt . Enc . ToStandardName ( item . Name ) )
2019-09-04 19:00:37 +00:00
o , err := f . newObjectWithInfo ( ctx , remote , item )
2018-08-01 23:02:35 +00:00
if err != nil {
continue
}
entries = append ( entries , o )
}
return entries , nil
}
2018-09-06 13:13:38 +00:00
// listFileDirFn is called from listFileDir to handle an object.
type listFileDirFn func ( fs . DirEntry ) error
// List the objects and directories into entries, from a
// special kind of JottaFolder representing a FileDirLis
2019-09-04 19:00:37 +00:00
func ( f * Fs ) listFileDir ( ctx context . Context , remoteStartPath string , startFolder * api . JottaFolder , fn listFileDirFn ) error {
2018-09-08 22:12:47 +00:00
pathPrefix := "/" + f . filePathRaw ( "" ) // Non-escaped prefix of API paths to be cut off, to be left with the remote path including the remoteStartPath
pathPrefixLength := len ( pathPrefix )
startPath := path . Join ( pathPrefix , remoteStartPath ) // Non-escaped API path up to and including remoteStartPath, to decide if it should be created as a new dir object
startPathLength := len ( startPath )
for i := range startFolder . Folders {
folder := & startFolder . Folders [ i ]
2018-09-06 13:13:38 +00:00
if folder . Deleted {
return nil
}
2020-01-14 17:33:35 +00:00
folderPath := f . opt . Enc . ToStandardPath ( path . Join ( folder . Path , folder . Name ) )
2018-10-14 18:17:41 +00:00
folderPathLength := len ( folderPath )
2018-09-06 13:13:38 +00:00
var remoteDir string
2018-10-14 18:17:41 +00:00
if folderPathLength > pathPrefixLength {
remoteDir = folderPath [ pathPrefixLength + 1 : ]
if folderPathLength > startPathLength {
2018-09-08 22:12:47 +00:00
d := fs . NewDir ( remoteDir , time . Time ( folder . ModifiedAt ) )
err := fn ( d )
if err != nil {
return err
}
2018-09-06 13:13:38 +00:00
}
}
for i := range folder . Files {
file := & folder . Files [ i ]
if file . Deleted || file . State != "COMPLETED" {
continue
}
2020-01-14 17:33:35 +00:00
remoteFile := path . Join ( remoteDir , f . opt . Enc . ToStandardName ( file . Name ) )
2019-09-04 19:00:37 +00:00
o , err := f . newObjectWithInfo ( ctx , remoteFile , file )
2018-09-06 13:13:38 +00:00
if err != nil {
return err
}
err = fn ( o )
if err != nil {
return err
}
}
}
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 { } ,
}
opts . Parameters . Set ( "mode" , "list" )
var resp * http . Response
var result api . JottaFolder // Could be JottaFileDirList, but JottaFolder is close enough
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 19:00:37 +00:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & result )
2018-09-06 13:13:38 +00:00
return shouldRetry ( resp , err )
} )
if err != nil {
if apiErr , ok := err . ( * api . Error ) ; ok {
// does not exist
if apiErr . StatusCode == http . StatusNotFound {
return fs . ErrorDirNotFound
}
}
return errors . Wrap ( err , "couldn't list files" )
}
list := walk . NewListRHelper ( callback )
2019-09-04 19:00:37 +00:00
err = f . listFileDir ( ctx , dir , & result , func ( entry fs . DirEntry ) error {
2018-09-06 13:13:38 +00:00
return list . Add ( entry )
} )
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
//
// Copy the reader in to the new object which is returned
//
// 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 ) {
2019-05-17 00:31:59 +00:00
if f . opt . Device != "Jotta" {
return nil , errors . New ( "upload not supported for devices other than Jotta" )
}
2019-06-17 08:34:30 +00:00
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
if strings . HasSuffix ( dirPath , "/" ) {
dirPath = dirPath [ : len ( dirPath ) - 1 ]
}
parent := path . Dir ( dirPath )
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 ) {
2019-09-04 19:00:37 +00:00
resp , err = f . srv . Call ( ctx , & opts )
2018-08-01 23:02:35 +00:00
return shouldRetry ( resp , err )
} )
if err != nil {
2018-09-04 19:02:35 +00:00
return errors . Wrap ( err , "couldn't purge directory" )
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
2019-06-17 08:34:30 +00:00
func ( f * Fs ) Purge ( ctx context . Context ) error {
return f . purgeCheck ( ctx , "" , false )
2018-08-01 23:02:35 +00:00
}
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 { } ,
}
2020-01-14 17:33:35 +00:00
opts . Parameters . Set ( method , "/" + path . Join ( f . endpointURL , f . opt . Enc . FromStandardPath ( path . Join ( f . root , dest ) ) ) )
2018-08-01 23:02:35 +00:00
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 19:00:37 +00:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & info )
2020-06-10 19:49:29 +00:00
retry , _ := shouldRetry ( resp , err )
return ( retry && resp . StatusCode != 500 ) , err
2018-08-01 23:02:35 +00:00
} )
if err != nil {
return nil , err
}
return info , nil
}
// Copy src to this remote using server side copy operations.
//
// This is stored with the remote path given
//
// It returns the destination Object and a possible error
//
// 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
if err != nil {
2018-09-04 19:02:35 +00:00
return nil , errors . Wrap ( err , "couldn't copy file" )
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)
}
// Move src to this remote using server side move operations.
//
// This is stored with the remote path given
//
// It returns the destination Object and a possible error
//
// 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 {
2018-09-04 19:02:35 +00:00
return nil , errors . Wrap ( err , "couldn't move file" )
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
// using server side move operations.
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantDirMove
//
// If destination exists then return fs.ErrorDirExists
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
}
2020-01-14 17:33:35 +00:00
_ , err = f . copyOrMove ( ctx , "mvDir" , path . Join ( f . endpointURL , f . opt . Enc . FromStandardPath ( srcPath ) ) + "/" , dstRemote )
2018-08-01 23:02:35 +00:00
2020-06-10 19:49:29 +00:00
// surprise! jottacloud fucked up dirmove - the api spits out an error but
// dir gets moved regardless
if apiErr , ok := err . ( * api . Error ) ; ok {
if apiErr . StatusCode == 500 {
_ , err := f . NewObject ( ctx , dstRemote )
if err == fs . ErrorNotAFile {
log . Printf ( "FIXME: ignoring DirMove error - move succeeded anyway\n" )
return nil
}
return err
}
}
2018-08-01 23:02:35 +00:00
if err != nil {
2018-09-04 19:02:35 +00:00
return errors . Wrap ( err , "couldn't move directory" )
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 ) {
2019-09-04 19:00:37 +00:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & result )
2018-09-04 19:02:35 +00:00
return shouldRetry ( resp , err )
} )
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 {
2018-09-04 19:02:35 +00:00
return "" , errors . Wrap ( err , "couldn't remove public link" )
}
return "" , errors . Wrap ( err , "couldn't create public link" )
}
2020-05-31 21:18:01 +00:00
if unlink {
2018-09-04 19:02:35 +00:00
if result . PublicSharePath != "" {
return "" , errors . Errorf ( "couldn't remove public link - %q" , result . PublicSharePath )
}
return "" , nil
}
if result . PublicSharePath == "" {
return "" , errors . New ( "couldn't create public link - no link path received" )
}
2018-12-29 23:53:18 +00:00
link = path . Join ( baseURL , result . PublicSharePath )
2018-09-04 19:02:35 +00:00
return link , nil
}
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 ) {
2019-09-04 19:00:37 +00:00
info , err := getDriveInfo ( ctx , f . srv , 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
}
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 {
return errors . Wrap ( err , "couldn't empty trash" )
}
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
}
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
2018-08-01 23:02:35 +00:00
o . modTime = time . Time ( info . ModifiedAt )
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
}
2020-04-11 10:11:20 +00:00
if bool ( info . Deleted ) && ! o . fs . opt . TrashedOnly {
2018-12-29 23:53:18 +00:00
return fs . ErrorObjectNotFound
}
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 {
2018-08-01 23:02:35 +00:00
return fs . ErrorCantSetModTime
}
// 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 ) {
2019-09-04 19:00:37 +00:00
resp , err = o . fs . srv . Call ( ctx , & opts )
2018-08-01 23:02:35 +00:00
return shouldRetry ( resp , err )
} )
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
tempFile , err = ioutil . TempFile ( "" , cachePrefix )
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
inData , err = ioutil . ReadAll ( teeReader )
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
//
// If existing is set then it updates the object rather than creating a new one
//
// 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 ) {
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 {
return errors . Wrap ( err , "failed to calculate MD5" )
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
}
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 ) ,
}
2019-06-17 08:34:30 +00:00
fileDate := api . Time ( src . ModTime ( ctx ) ) . APIString ( )
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 ,
Created : fileDate ,
Modified : fileDate ,
Md5 : md5String ,
2020-01-14 17:33:35 +00:00
Path : path . Join ( o . fs . opt . Mountpoint , o . fs . opt . Enc . FromStandardPath ( path . Join ( o . fs . root , o . remote ) ) ) ,
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 )
2018-08-01 23:02:35 +00:00
return shouldRetry ( resp , err )
} )
if err != nil {
return err
}
2018-12-29 23:53:18 +00:00
// If the file state is INCOMPLETE and CORRPUT, try to upload a then
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
_ , err = io . CopyN ( ioutil . Discard , in , response . ResumePos )
if err != nil {
return err
}
// send the remaining bytes
2019-09-04 19:00:37 +00:00
resp , err = o . fs . apiSrv . CallJSON ( ctx , & opts , nil , & result )
2018-08-22 20:26:18 +00:00
if err != nil {
return err
}
// finally update the meta data
o . hasMetaData = true
2019-01-11 17:17:46 +00:00
o . size = result . Bytes
2018-08-22 20:26:18 +00:00
o . md5 = result . Md5
o . modTime = time . Unix ( result . Modified / 1000 , 0 )
2018-12-29 23:53:18 +00:00
} else {
2019-08-14 17:39:19 +00:00
// If the file state is COMPLETE we don't need to upload it because the file was already found but we still ned to update our metadata
2019-09-04 19:00:37 +00:00
return o . readMetaData ( ctx , true )
2018-08-22 20:26:18 +00:00
}
return nil
2018-08-01 23:02:35 +00:00
}
// Remove an object
2019-06-17 08:34:30 +00:00
func ( o * Object ) Remove ( ctx context . Context ) 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
}
2018-09-07 11:58:18 +00:00
if o . fs . opt . HardDelete {
opts . Parameters . Set ( "rm" , "true" )
} else {
opts . Parameters . Set ( "dl" , "true" )
}
2018-08-01 23:02:35 +00:00
return o . fs . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 19:00:37 +00:00
resp , err := o . fs . srv . CallXML ( ctx , & opts , nil , nil )
2018-08-01 23:02:35 +00:00
return shouldRetry ( resp , err )
} )
}
// 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 )
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 )
2018-08-01 23:02:35 +00:00
)