2018-08-02 01:02:35 +02:00
package jottacloud
import (
2018-08-14 23:15:21 +02:00
"bytes"
2019-06-17 10:34:30 +02:00
"context"
2018-08-14 23:15:21 +02:00
"crypto/md5"
"encoding/hex"
2018-08-02 01:02:35 +02:00
"fmt"
"io"
2018-08-14 23:15:21 +02:00
"io/ioutil"
2019-01-28 21:50:51 +01:00
"log"
2019-04-07 16:12:32 +02:00
"math/rand"
2018-08-02 01:02:35 +02:00
"net/http"
"net/url"
2018-08-14 23:15:21 +02:00
"os"
2018-08-02 01:02:35 +02:00
"path"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
2019-07-28 19:47:38 +02: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"
"github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/fshttp"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/walk"
"github.com/rclone/rclone/lib/oauthutil"
"github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/rest"
2018-08-22 22:26:18 +02:00
"golang.org/x/oauth2"
2018-08-02 01:02:35 +02:00
)
// Globals
const (
2018-08-22 22:26:18 +02:00
minSleep = 10 * time . Millisecond
maxSleep = 2 * time . Second
decayConstant = 2 // bigger for slower decay, exponential
defaultDevice = "Jotta"
2019-05-17 02:31:59 +02:00
defaultMountpoint = "Archive"
2018-08-22 22:26:18 +02:00
rootURL = "https://www.jottacloud.com/jfs/"
2019-08-13 15:28:46 +02:00
apiURL = "https://api.jottacloud.com/"
2018-12-30 00:53:18 +01:00
baseURL = "https://www.jottacloud.com/"
2018-08-22 22:26:18 +02:00
tokenURL = "https://api.jottacloud.com/auth/v1/token"
2019-04-07 16:12:32 +02:00
registerURL = "https://api.jottacloud.com/auth/v1/register"
2018-08-22 22:26:18 +02:00
cachePrefix = "rclone-jcmd5-"
rcloneClientID = "nibfk8biu12ju7hpqomr8b1e40"
rcloneEncryptedClientSecret = "Vp8eAv7eVElMnQwN-kgU9cbhgApNDaMqWdlDi5qFydlQoji4JBxrGMF2"
2019-04-07 16:12:32 +02:00
configClientID = "client_id"
configClientSecret = "client_secret"
2019-05-17 02:31:59 +02:00
configDevice = "device"
configMountpoint = "mountpoint"
2019-04-07 16:12:32 +02:00
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
2018-08-22 22:26:18 +02:00
)
var (
// Description of how to auth for this app for a personal account
oauthConfig = & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
AuthURL : tokenURL ,
TokenURL : tokenURL ,
} ,
2019-04-07 16:12:32 +02:00
RedirectURL : oauthutil . RedirectLocalhostURL ,
2018-08-22 22:26:18 +02:00
}
2018-08-02 01:02:35 +02:00
)
// Register with Fs
func init ( ) {
2019-05-17 02:31:59 +02:00
// needs to be done early so we can use oauth during config
2018-08-02 01:02:35 +02:00
fs . Register ( & fs . RegInfo {
Name : "jottacloud" ,
Description : "JottaCloud" ,
NewFs : NewFs ,
2018-08-22 22:26:18 +02:00
Config : func ( name string , m configmap . Mapper ) {
2019-09-04 21:00:37 +02:00
ctx := context . TODO ( )
2018-12-30 20:22:33 +01:00
tokenString , ok := m . Get ( "token" )
if ok && tokenString != "" {
fmt . Printf ( "Already have a token - refresh?\n" )
if ! config . Confirm ( ) {
return
}
}
2019-04-07 16:12:32 +02:00
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 ( ) {
2019-09-04 21:00:37 +02:00
deviceRegistration , err := registerDevice ( ctx , srv )
2019-04-07 16:12:32 +02:00
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 = rcloneClientID
}
clientSecret , ok := m . Get ( configClientSecret )
if ! ok {
clientSecret = rcloneEncryptedClientSecret
}
oauthConfig . ClientID = clientID
oauthConfig . ClientSecret = obscure . MustReveal ( clientSecret )
2019-08-14 19:39:19 +02:00
fmt . Printf ( "Username> " )
username := config . ReadLine ( )
2019-05-17 02:31:59 +02:00
password := config . GetPassword ( "Your Jottacloud password is only required during setup and will not be stored." )
2018-08-22 22:26:18 +02:00
2019-09-04 21:00:37 +02:00
token , err := doAuth ( ctx , srv , username , password )
2018-08-22 22:26:18 +02:00
if err != nil {
2019-08-14 19:39:19 +02:00
log . Fatalf ( "Failed to get oauth token: %s" , err )
2018-08-22 22:26:18 +02:00
}
err = oauthutil . PutToken ( name , m , & token , true )
if err != nil {
2019-05-17 02:31:59 +02:00
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 ( ) {
oAuthClient , _ , err := oauthutil . NewClient ( name , m , oauthConfig )
if err != nil {
log . Fatalf ( "Failed to load oAuthClient: %s" , err )
}
srv = rest . NewClient ( oAuthClient ) . SetRoot ( rootURL )
2019-08-13 15:28:46 +02:00
apiSrv := rest . NewClient ( oAuthClient ) . SetRoot ( apiURL )
2019-05-17 02:31:59 +02:00
2019-09-04 21:00:37 +02:00
device , mountpoint , err := setupMountpoint ( ctx , srv , apiSrv )
2019-08-13 15:28:46 +02:00
if err != nil {
2019-08-14 19:39:19 +02:00
log . Fatalf ( "Failed to setup mountpoint: %s" , err )
2019-08-13 15:28:46 +02:00
}
2019-08-14 19:39:19 +02:00
m . Set ( configDevice , device )
m . Set ( configMountpoint , mountpoint )
2018-08-22 22:26:18 +02:00
}
} ,
2018-08-02 01:02:35 +02:00
Options : [ ] fs . Option { {
2018-08-14 23:15:21 +02:00
Name : "md5_memory_limit" ,
2018-08-20 16:38:21 +02:00
Help : "Files bigger than this will be cached on disk to calculate the MD5 if required." ,
2018-08-14 23:15:21 +02:00
Default : fs . SizeSuffix ( 10 * 1024 * 1024 ) ,
Advanced : true ,
2018-09-07 13:58:18 +02:00
} , {
Name : "hard_delete" ,
Help : "Delete files permanently rather than putting them into the trash." ,
Default : false ,
Advanced : true ,
2018-09-04 21:02:35 +02:00
} , {
Name : "unlink" ,
2018-10-01 19:36:15 +02:00
Help : "Remove existing public link to file/folder with link command rather than creating.\nDefault is false, meaning link command will create or retrieve public link." ,
2018-09-04 21:02:35 +02:00
Default : false ,
Advanced : true ,
2018-08-22 22:26:18 +02:00
} , {
Name : "upload_resume_limit" ,
2019-02-07 18:41:17 +01:00
Help : "Files bigger than this can be resumed if the upload fail's." ,
2018-08-22 22:26:18 +02:00
Default : fs . SizeSuffix ( 10 * 1024 * 1024 ) ,
Advanced : true ,
2018-08-02 01:02:35 +02:00
} } ,
} )
}
// Options defines the configuration for this backend
type Options struct {
2019-05-17 02:31:59 +02:00
Device string ` config:"device" `
2018-08-14 23:15:21 +02:00
Mountpoint string ` config:"mountpoint" `
MD5MemoryThreshold fs . SizeSuffix ` config:"md5_memory_limit" `
2018-09-07 13:58:18 +02:00
HardDelete bool ` config:"hard_delete" `
2018-09-04 21:02:35 +02:00
Unlink bool ` config:"unlink" `
2018-08-22 22:26:18 +02:00
UploadThreshold fs . SizeSuffix ` config:"upload_resume_limit" `
2018-08-02 01:02:35 +02:00
}
// Fs represents a remote jottacloud
type Fs struct {
2018-08-22 22:26:18 +02:00
name string
root string
user string
opt Options
features * fs . Features
endpointURL string
srv * rest . Client
apiSrv * rest . Client
2019-02-09 21:52:15 +01:00
pacer * fs . Pacer
2018-08-22 22:26:18 +02:00
tokenRenewer * oauthutil . Renew // renew the token on expiry
2018-08-02 01:02:35 +02: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-15 00:33:58 +02:00
mimeType string
2018-08-02 01:02:35 +02: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
}
// parsePath parses an box 'url'
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
}
2019-08-14 19:39:19 +02:00
// registerDevice register a new device for use with the jottacloud API
2019-09-04 21:00:37 +02:00
func registerDevice ( ctx context . Context , srv * rest . Client ) ( reg * api . DeviceRegistrationResponse , err error ) {
2019-08-14 19:39:19 +02:00
// random generator to generate random device names
seededRand := rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) )
randonDeviceNamePartLength := 21
randomDeviceNamePart := make ( [ ] byte , randonDeviceNamePartLength )
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 )
2018-08-02 01:02:35 +02:00
opts := rest . Opts {
2019-08-14 19:39:19 +02:00
Method : "POST" ,
RootURL : registerURL ,
ContentType : "application/x-www-form-urlencoded" ,
ExtraHeaders : map [ string ] string { "Authorization" : "Bearer c2xrZmpoYWRsZmFramhkc2xma2phaHNkbGZramhhc2xkZmtqaGFzZGxrZmpobGtq" } ,
Parameters : values ,
2018-08-02 01:02:35 +02:00
}
2019-08-14 19:39:19 +02:00
var deviceRegistration * api . DeviceRegistrationResponse
2019-09-04 21:00:37 +02:00
_ , err = srv . CallJSON ( ctx , & opts , nil , & deviceRegistration )
2019-08-14 19:39:19 +02:00
return deviceRegistration , err
}
// doAuth runs the actual token request
2019-09-04 21:00:37 +02:00
func doAuth ( ctx context . Context , srv * rest . Client , username , password string ) ( token oauth2 . Token , err error ) {
2019-08-14 19:39:19 +02: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 ,
}
// do the first request
var jsonToken api . TokenJSON
2019-09-04 21:00:37 +02:00
resp , err := srv . CallJSON ( ctx , & opts , nil , & jsonToken )
2019-08-14 19:39:19 +02:00
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
2019-09-04 21:00:37 +02:00
resp , err = srv . CallJSON ( ctx , & opts , nil , & jsonToken )
2019-08-14 19:39:19 +02:00
}
2018-08-02 01:02:35 +02:00
}
}
2019-08-14 19:39:19 +02: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 21:00:37 +02: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 19:39:19 +02:00
if err != nil {
return "" , "" , err
}
2019-09-04 21:00:37 +02:00
acc , err := getDriveInfo ( ctx , srv , cust . Username )
2018-08-02 01:02:35 +02:00
if err != nil {
2019-08-14 19:39:19 +02:00
return "" , "" , err
2018-08-02 01:02:35 +02:00
}
2019-08-14 19:39:19 +02:00
var deviceNames [ ] string
for i := range acc . Devices {
deviceNames = append ( deviceNames , acc . Devices [ i ] . Name )
2018-08-02 01:02:35 +02:00
}
2019-08-14 19:39:19 +02: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 21:00:37 +02:00
dev , err := getDeviceInfo ( ctx , srv , path . Join ( cust . Username , device ) )
2019-08-14 19:39:19 +02: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-02 01:02:35 +02:00
}
2019-08-14 19:39:19 +02:00
// getCustomerInfo queries general information about the account
2019-09-04 21:00:37 +02:00
func getCustomerInfo ( ctx context . Context , srv * rest . Client ) ( info * api . CustomerInfo , err error ) {
2019-08-13 15:28:46 +02:00
opts := rest . Opts {
Method : "GET" ,
Path : "account/v1/customer" ,
}
2019-09-04 21:00:37 +02:00
_ , err = srv . CallJSON ( ctx , & opts , nil , & info )
2019-08-13 15:28:46 +02:00
if err != nil {
2019-08-14 19:39:19 +02:00
return nil , errors . Wrap ( err , "couldn't get customer info" )
2019-08-13 15:28:46 +02:00
}
return info , nil
}
2019-08-14 19:39:19 +02:00
// getDriveInfo queries general information about the account and the available devices and mountpoints.
2019-09-04 21:00:37 +02:00
func getDriveInfo ( ctx context . Context , srv * rest . Client , username string ) ( info * api . DriveInfo , err error ) {
2018-08-02 01:02:35 +02:00
opts := rest . Opts {
Method : "GET" ,
2019-08-13 15:28:46 +02:00
Path : username ,
2018-08-02 01:02:35 +02:00
}
2019-09-04 21:00:37 +02:00
_ , err = srv . CallXML ( ctx , & opts , nil , & info )
2019-05-17 02:31:59 +02:00
if err != nil {
2019-08-14 19:39:19 +02:00
return nil , errors . Wrap ( err , "couldn't get drive info" )
2019-05-17 02:31:59 +02:00
}
return info , nil
}
// getDeviceInfo queries Information about a jottacloud device
2019-09-04 21:00:37 +02:00
func getDeviceInfo ( ctx context . Context , srv * rest . Client , path string ) ( info * api . JottaDevice , err error ) {
2019-05-17 02:31:59 +02:00
opts := rest . Opts {
Method : "GET" ,
Path : urlPathEscape ( path ) ,
}
2019-09-04 21:00:37 +02:00
_ , err = srv . CallXML ( ctx , & opts , nil , & info )
2018-08-02 01:02:35 +02:00
if err != nil {
2019-08-14 19:39:19 +02:00
return nil , errors . Wrap ( err , "couldn't get device info" )
2018-08-02 01:02:35 +02:00
}
2018-08-15 01:12:20 +02:00
return info , nil
}
2019-08-14 19:39:19 +02:00
// setEndpointURL generates the API endpoint URL
2019-08-13 15:28:46 +02:00
func ( f * Fs ) setEndpointURL ( ) {
2019-05-17 02:31:59 +02:00
if f . opt . Device == "" {
f . opt . Device = defaultDevice
}
if f . opt . Mountpoint == "" {
f . opt . Mountpoint = defaultMountpoint
}
2019-08-13 15:28:46 +02:00
f . endpointURL = urlPathEscape ( path . Join ( f . user , f . opt . Device , f . opt . Mountpoint ) )
2018-08-02 01:02:35 +02:00
}
2019-08-14 19:39:19 +02:00
// readMetaDataForPath reads the metadata from the path
2019-09-04 21:00:37 +02:00
func ( f * Fs ) readMetaDataForPath ( ctx context . Context , path string ) ( info * api . JottaFile , err error ) {
2019-08-14 19:39:19 +02: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 21:00:37 +02:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & result )
2019-08-14 19:39:19 +02: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-02 01:02:35 +02: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
}
2018-09-16 22:30:20 +02:00
// Jottacloud want's '+' to be URL encoded even though the RFC states it's not reserved
func urlPathEscape ( in string ) string {
return strings . Replace ( rest . URLPathEscape ( in ) , "+" , "%2B" , - 1 )
}
2018-09-06 15:13:38 +02:00
// filePathRaw returns an unescaped file path (f.root, file)
func ( f * Fs ) filePathRaw ( file string ) string {
return path . Join ( f . endpointURL , replaceReservedChars ( path . Join ( f . root , file ) ) )
}
2018-08-02 01:02:35 +02:00
// filePath returns a escaped file path (f.root, file)
func ( f * Fs ) filePath ( file string ) string {
2018-09-16 22:30:20 +02:00
return urlPathEscape ( f . filePathRaw ( file ) )
2018-08-02 01:02:35 +02:00
}
2018-08-22 22:26:18 +02: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 tokenURL == req . URL . String ( ) {
// read the entire body
refreshBody , err := ioutil . ReadAll ( req . Body )
if err != nil {
return
}
_ = req . Body . Close ( )
2019-02-07 18:41:17 +01:00
// make the refresh token upper case
2018-08-22 22:26:18 +02:00
refreshBody = [ ] byte ( strings . Replace ( string ( refreshBody ) , "grant_type=refresh_token" , "grant_type=REFRESH_TOKEN" , 1 ) )
// set the new ReadCloser (with a dummy Close())
2018-12-30 20:22:33 +01:00
req . Body = ioutil . NopCloser ( bytes . NewReader ( refreshBody ) )
2018-08-22 22:26:18 +02:00
}
}
2018-08-02 01:02:35 +02:00
// NewFs constructs an Fs from the path, container:path
func NewFs ( name , root string , m configmap . Mapper ) ( fs . Fs , error ) {
2019-09-04 21:00:37 +02:00
ctx := context . TODO ( )
2018-08-02 01:02:35 +02:00
// Parse config into Options struct
opt := new ( Options )
err := configstruct . Set ( m , opt )
if err != nil {
return nil , err
}
rootIsDir := strings . HasSuffix ( root , "/" )
root = parsePath ( root )
2019-04-07 16:12:32 +02:00
clientID , ok := m . Get ( configClientID )
if ! ok {
clientID = rcloneClientID
}
clientSecret , ok := m . Get ( configClientSecret )
if ! ok {
clientSecret = rcloneEncryptedClientSecret
}
oauthConfig . ClientID = clientID
oauthConfig . ClientSecret = obscure . MustReveal ( clientSecret )
2018-08-22 22:26:18 +02:00
// the oauth client for the api servers needs
// a filter to fix the grant_type issues (see above)
baseClient := fshttp . NewClient ( fs . Config )
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" )
}
oAuthClient , ts , err := oauthutil . NewClientWithBaseClient ( name , m , oauthConfig , baseClient )
if err != nil {
return nil , errors . Wrap ( err , "Failed to configure Jottacloud oauth client" )
}
2018-08-02 01:02:35 +02:00
f := & Fs {
2019-01-28 21:50:51 +01:00
name : name ,
root : root ,
opt : * opt ,
2018-12-30 00:53:18 +01:00
srv : rest . NewClient ( oAuthClient ) . SetRoot ( rootURL ) ,
2018-08-22 22:26:18 +02:00
apiSrv : rest . NewClient ( oAuthClient ) . SetRoot ( apiURL ) ,
2019-02-09 21:52:15 +01:00
pacer : fs . NewPacer ( pacer . NewDefault ( pacer . MinSleep ( minSleep ) , pacer . MaxSleep ( maxSleep ) , pacer . DecayConstant ( decayConstant ) ) ) ,
2018-08-02 01:02:35 +02:00
}
f . features = ( & fs . Features {
CaseInsensitive : true ,
CanHaveEmptyDirectories : true ,
2018-08-15 01:12:20 +02:00
ReadMimeType : true ,
WriteMimeType : true ,
2018-08-02 01:02:35 +02:00
} ) . Fill ( f )
2018-08-22 22:26:18 +02:00
f . srv . SetErrorHandler ( errorHandler )
// Renew the token in the background
f . tokenRenewer = oauthutil . NewRenew ( f . String ( ) , ts , func ( ) error {
2019-09-04 21:00:37 +02:00
_ , err := f . readMetaDataForPath ( ctx , "" )
2018-08-22 22:26:18 +02:00
return err
} )
2019-09-04 21:00:37 +02:00
cust , err := getCustomerInfo ( ctx , f . apiSrv )
2018-08-02 01:02:35 +02:00
if err != nil {
2019-08-14 19:39:19 +02:00
return nil , err
2018-08-02 01:02:35 +02:00
}
2019-08-13 15:28:46 +02:00
f . user = cust . Username
f . setEndpointURL ( )
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
_ , err := f . NewObject ( context . TODO ( ) , remote )
2018-08-02 01:02:35 +02: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 21:00:37 +02:00
func ( f * Fs ) newObjectWithInfo ( ctx context . Context , remote string , info * api . JottaFile ) ( fs . Object , error ) {
2018-08-02 01:02:35 +02:00
o := & Object {
fs : f ,
remote : remote ,
}
var err error
if info != nil {
// Set info
err = o . setMetaData ( info )
} else {
2019-09-04 21:00:37 +02:00
err = o . readMetaData ( ctx , false ) // reads info and meta, returning an error
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( f * Fs ) NewObject ( ctx context . Context , remote string ) ( fs . Object , error ) {
2019-09-04 21:00:37 +02:00
return f . newObjectWithInfo ( ctx , remote , nil )
2018-08-02 01:02:35 +02:00
}
// CreateDir makes a directory
2019-09-04 21:00:37 +02:00
func ( f * Fs ) CreateDir ( ctx context . Context , path string ) ( jf * api . JottaFolder , err error ) {
2018-08-02 01:02:35 +02: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 21:00:37 +02:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & jf )
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( f * Fs ) List ( ctx context . Context , dir string ) ( entries fs . DirEntries , err error ) {
2018-08-02 01:02:35 +02: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 21:00:37 +02:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & result )
2018-08-02 01:02:35 +02: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" )
}
if result . Deleted {
return nil , fs . ErrorDirNotFound
}
for i := range result . Folders {
item := & result . Folders [ i ]
if item . Deleted {
continue
}
remote := path . Join ( dir , restoreReservedChars ( item . Name ) )
d := fs . NewDir ( remote , time . Time ( item . ModifiedAt ) )
entries = append ( entries , d )
}
for i := range result . Files {
item := & result . Files [ i ]
if item . Deleted || item . State != "COMPLETED" {
continue
}
remote := path . Join ( dir , restoreReservedChars ( item . Name ) )
2019-09-04 21:00:37 +02:00
o , err := f . newObjectWithInfo ( ctx , remote , item )
2018-08-02 01:02:35 +02:00
if err != nil {
continue
}
entries = append ( entries , o )
}
return entries , nil
}
2018-09-06 15:13:38 +02: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 21:00:37 +02:00
func ( f * Fs ) listFileDir ( ctx context . Context , remoteStartPath string , startFolder * api . JottaFolder , fn listFileDirFn ) error {
2018-09-09 00:12:47 +02: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 15:13:38 +02:00
if folder . Deleted {
return nil
}
2018-10-14 20:17:41 +02:00
folderPath := restoreReservedChars ( path . Join ( folder . Path , folder . Name ) )
folderPathLength := len ( folderPath )
2018-09-06 15:13:38 +02:00
var remoteDir string
2018-10-14 20:17:41 +02:00
if folderPathLength > pathPrefixLength {
remoteDir = folderPath [ pathPrefixLength + 1 : ]
if folderPathLength > startPathLength {
2018-09-09 00:12:47 +02:00
d := fs . NewDir ( remoteDir , time . Time ( folder . ModifiedAt ) )
err := fn ( d )
if err != nil {
return err
}
2018-09-06 15:13:38 +02:00
}
}
for i := range folder . Files {
file := & folder . Files [ i ]
if file . Deleted || file . State != "COMPLETED" {
continue
}
remoteFile := path . Join ( remoteDir , restoreReservedChars ( file . Name ) )
2019-09-04 21:00:37 +02:00
o , err := f . newObjectWithInfo ( ctx , remoteFile , file )
2018-09-06 15:13:38 +02: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 10:34:30 +02:00
func ( f * Fs ) ListR ( ctx context . Context , dir string , callback fs . ListRCallback ) ( err error ) {
2018-09-06 15:13:38 +02: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 21:00:37 +02:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & result )
2018-09-06 15:13:38 +02: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 21:00:37 +02:00
err = f . listFileDir ( ctx , dir , & result , func ( entry fs . DirEntry ) error {
2018-09-06 15:13:38 +02:00
return list . Add ( entry )
} )
if err != nil {
return err
}
return list . Flush ( )
}
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( f * Fs ) Put ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
2019-05-17 02:31:59 +02:00
if f . opt . Device != "Jotta" {
return nil , errors . New ( "upload not supported for devices other than Jotta" )
}
2019-06-17 10:34:30 +02:00
o := f . createObject ( src . Remote ( ) , src . ModTime ( ctx ) , src . Size ( ) )
return o , o . Update ( ctx , in , src , options ... )
2018-08-02 01:02:35 +02:00
}
// mkParentDir makes the parent of the native path dirPath if
// necessary and any directories above that
2019-06-17 10:34:30 +02:00
func ( f * Fs ) mkParentDir ( ctx context . Context , dirPath string ) error {
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
return f . Mkdir ( ctx , parent )
2018-08-02 01:02:35 +02:00
}
// Mkdir creates the container if it doesn't exist
2019-06-17 10:34:30 +02:00
func ( f * Fs ) Mkdir ( ctx context . Context , dir string ) error {
2019-09-04 21:00:37 +02:00
_ , err := f . CreateDir ( ctx , dir )
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( f * Fs ) purgeCheck ( ctx context . Context , dir string , check bool ) ( err error ) {
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
entries , err := f . List ( ctx , dir )
2018-08-02 01:02:35 +02: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 13:58:18 +02:00
if f . opt . HardDelete {
opts . Parameters . Set ( "rmDir" , "true" )
} else {
opts . Parameters . Set ( "dlDir" , "true" )
}
2018-08-02 01:02:35 +02:00
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . Call ( ctx , & opts )
2018-08-02 01:02:35 +02:00
return shouldRetry ( resp , err )
} )
if err != nil {
2018-09-04 21:02:35 +02:00
return errors . Wrap ( err , "couldn't purge directory" )
2018-08-02 01:02:35 +02:00
}
return nil
}
// Rmdir deletes the root folder
//
// Returns an error if it isn't empty
2019-06-17 10:34:30 +02:00
func ( f * Fs ) Rmdir ( ctx context . Context , dir string ) error {
return f . purgeCheck ( ctx , dir , true )
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( f * Fs ) Purge ( ctx context . Context ) error {
return f . purgeCheck ( ctx , "" , false )
2018-08-02 01:02:35 +02:00
}
2019-02-07 18:41:17 +01:00
// copyOrMoves copies or moves directories or files depending on the method parameter
2019-09-04 21:00:37 +02:00
func ( f * Fs ) copyOrMove ( ctx context . Context , method , src , dest string ) ( info * api . JottaFile , err error ) {
2018-08-02 01:02:35 +02:00
opts := rest . Opts {
Method : "POST" ,
Path : src ,
Parameters : url . Values { } ,
}
opts . Parameters . Set ( method , "/" + path . Join ( f . endpointURL , replaceReservedChars ( path . Join ( f . root , dest ) ) ) )
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & info )
2018-08-02 01:02:35 +02:00
return shouldRetry ( resp , err )
} )
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 10:34:30 +02:00
func ( f * Fs ) Copy ( ctx context . Context , src fs . Object , remote string ) ( fs . Object , error ) {
2018-08-02 01:02:35 +02:00
srcObj , ok := src . ( * Object )
if ! ok {
fs . Debugf ( src , "Can't copy - not same remote type" )
return nil , fs . ErrorCantMove
}
2019-06-17 10:34:30 +02:00
err := f . mkParentDir ( ctx , remote )
2018-08-02 01:02:35 +02:00
if err != nil {
return nil , err
}
2019-09-04 21:00:37 +02:00
info , err := f . copyOrMove ( ctx , "cp" , srcObj . filePath ( ) , remote )
2018-08-02 01:02:35 +02:00
if err != nil {
2018-09-04 21:02:35 +02:00
return nil , errors . Wrap ( err , "couldn't copy file" )
2018-08-02 01:02:35 +02:00
}
2019-09-04 21:00:37 +02:00
return f . newObjectWithInfo ( ctx , remote , info )
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( f * Fs ) Move ( ctx context . Context , src fs . Object , remote string ) ( fs . Object , error ) {
2018-08-02 01:02:35 +02:00
srcObj , ok := src . ( * Object )
if ! ok {
fs . Debugf ( src , "Can't move - not same remote type" )
return nil , fs . ErrorCantMove
}
2019-06-17 10:34:30 +02:00
err := f . mkParentDir ( ctx , remote )
2018-08-02 01:02:35 +02:00
if err != nil {
return nil , err
}
2019-09-04 21:00:37 +02:00
info , err := f . copyOrMove ( ctx , "mv" , srcObj . filePath ( ) , remote )
2018-08-02 01:02:35 +02:00
if err != nil {
2018-09-04 21:02:35 +02:00
return nil , errors . Wrap ( err , "couldn't move file" )
2018-08-02 01:02:35 +02:00
}
2019-09-04 21:00:37 +02:00
return f . newObjectWithInfo ( ctx , remote , info )
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( f * Fs ) DirMove ( ctx context . Context , src fs . Fs , srcRemote , dstRemote string ) error {
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
_ , err = f . List ( ctx , dstRemote )
2018-08-02 01:02:35 +02:00
if err == fs . ErrorDirNotFound {
// OK
} else if err != nil {
return err
} else {
return fs . ErrorDirExists
}
2019-09-04 21:00:37 +02:00
_ , err = f . copyOrMove ( ctx , "mvDir" , path . Join ( f . endpointURL , replaceReservedChars ( srcPath ) ) + "/" , dstRemote )
2018-08-02 01:02:35 +02:00
if err != nil {
2018-09-04 21:02:35 +02:00
return errors . Wrap ( err , "couldn't move directory" )
2018-08-02 01:02:35 +02:00
}
return nil
}
2018-09-04 21:02:35 +02:00
// PublicLink generates a public link to the remote path (usually readable by anyone)
2019-06-17 10:34:30 +02:00
func ( f * Fs ) PublicLink ( ctx context . Context , remote string ) ( link string , err error ) {
2018-09-04 21:02:35 +02:00
opts := rest . Opts {
Method : "GET" ,
Path : f . filePath ( remote ) ,
Parameters : url . Values { } ,
}
if f . opt . Unlink {
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 21:00:37 +02:00
resp , err = f . srv . CallXML ( ctx , & opts , nil , & result )
2018-09-04 21:02:35 +02: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 {
if f . opt . Unlink {
return "" , errors . Wrap ( err , "couldn't remove public link" )
}
return "" , errors . Wrap ( err , "couldn't create public link" )
}
if f . opt . Unlink {
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-30 00:53:18 +01:00
link = path . Join ( baseURL , result . PublicSharePath )
2018-09-04 21:02:35 +02:00
return link , nil
}
2018-08-15 01:12:20 +02:00
// About gets quota information
2019-06-17 10:34:30 +02:00
func ( f * Fs ) About ( ctx context . Context ) ( * fs . Usage , error ) {
2019-09-04 21:00:37 +02:00
info , err := getDriveInfo ( ctx , f . srv , f . user )
2018-08-15 01:12:20 +02:00
if err != nil {
return nil , err
}
usage := & fs . Usage {
2018-09-06 15:13:38 +02:00
Used : fs . NewUsageValue ( info . Usage ) ,
2018-08-15 01:12:20 +02:00
}
2018-09-06 19:55:16 +02:00
if info . Capacity > 0 {
usage . Total = fs . NewUsageValue ( info . Capacity )
usage . Free = fs . NewUsageValue ( info . Capacity - info . Usage )
}
2018-08-15 01:12:20 +02:00
return usage , nil
}
2018-08-02 01:02:35 +02: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
}
2019-08-14 19:39:19 +02:00
// filePath returns a escaped file path (f.root, remote)
func ( o * Object ) filePath ( ) string {
return o . fs . filePath ( o . remote )
}
2018-08-02 01:02:35 +02:00
// Hash returns the MD5 of an object returning a lowercase hex string
2019-06-17 10:34:30 +02:00
func ( o * Object ) Hash ( ctx context . Context , t hash . Type ) ( string , error ) {
2018-08-02 01:02:35 +02: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 21:00:37 +02:00
ctx := context . TODO ( )
err := o . readMetaData ( ctx , false )
2018-08-02 01:02:35 +02:00
if err != nil {
fs . Logf ( o , "Failed to read metadata: %v" , err )
return 0
}
return o . size
}
2018-08-15 00:33:58 +02:00
// MimeType of an Object if known, "" otherwise
2019-06-17 10:34:30 +02:00
func ( o * Object ) MimeType ( ctx context . Context ) string {
2018-08-15 00:33:58 +02:00
return o . mimeType
}
2018-08-02 01:02:35 +02:00
// setMetaData sets the metadata from info
func ( o * Object ) setMetaData ( info * api . JottaFile ) ( err error ) {
o . hasMetaData = true
2019-01-11 18:17:46 +01:00
o . size = info . Size
2018-08-02 01:02:35 +02:00
o . md5 = info . MD5
2018-08-15 00:33:58 +02:00
o . mimeType = info . MimeType
2018-08-02 01:02:35 +02:00
o . modTime = time . Time ( info . ModifiedAt )
return nil
}
2019-08-14 19:39:19 +02:00
// readMetaData reads and updates the metadata for an object
2019-09-04 21:00:37 +02:00
func ( o * Object ) readMetaData ( ctx context . Context , force bool ) ( err error ) {
2018-12-30 00:53:18 +01:00
if o . hasMetaData && ! force {
2018-08-02 01:02:35 +02:00
return nil
}
2019-09-04 21:00:37 +02:00
info , err := o . fs . readMetaDataForPath ( ctx , o . remote )
2018-08-02 01:02:35 +02:00
if err != nil {
return err
}
2018-12-30 00:53:18 +01:00
if info . Deleted {
return fs . ErrorObjectNotFound
}
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( o * Object ) ModTime ( ctx context . Context ) time . Time {
2019-09-04 21:00:37 +02:00
err := o . readMetaData ( ctx , false )
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( o * Object ) SetModTime ( ctx context . Context , modTime time . Time ) error {
2018-08-02 01:02:35 +02: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 10:34:30 +02:00
func ( o * Object ) Open ( ctx context . Context , options ... fs . OpenOption ) ( in io . ReadCloser , err error ) {
2018-08-02 01:02:35 +02: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 21:00:37 +02:00
resp , err = o . fs . srv . Call ( ctx , & opts )
2018-08-02 01:02:35 +02:00
return shouldRetry ( resp , err )
} )
if err != nil {
return nil , err
}
return resp . Body , err
}
2018-08-20 16:38:21 +02: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 ) {
// we need a MD5
md5Hasher := md5 . New ( )
2019-02-07 18:41:17 +01:00
// use the teeReader to write to the local file AND calculate the MD5 while doing so
2018-08-20 16:38:21 +02: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-02 01:02:35 +02: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 10:34:30 +02:00
func ( o * Object ) Update ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( err error ) {
2018-08-20 16:38:21 +02:00
size := src . Size ( )
2019-06-17 10:34:30 +02:00
md5String , err := src . Hash ( ctx , hash . MD5 )
2018-08-14 23:15:21 +02:00
if err != nil || md5String == "" {
2018-08-20 16:38:21 +02: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 23:15:21 +02:00
}
2018-08-20 16:38:21 +02:00
// Wrap the accounting back onto the stream
in = wrap ( in )
2018-08-14 23:15:21 +02:00
}
2018-08-22 22:26:18 +02:00
// use the api to allocate the file first and get resume / deduplication info
2018-12-30 00:53:18 +01:00
var resp * http . Response
2018-08-02 01:02:35 +02:00
opts := rest . Opts {
2018-08-22 22:26:18 +02:00
Method : "POST" ,
2019-08-13 15:28:46 +02:00
Path : "files/v1/allocate" ,
2018-08-22 22:26:18 +02:00
ExtraHeaders : make ( map [ string ] string ) ,
}
2019-06-17 10:34:30 +02:00
fileDate := api . Time ( src . ModTime ( ctx ) ) . APIString ( )
2018-08-02 01:02:35 +02:00
2018-08-22 22:26:18 +02:00
// the allocate request
var request = api . AllocateFileRequest {
Bytes : size ,
Created : fileDate ,
Modified : fileDate ,
Md5 : md5String ,
2018-12-30 00:53:18 +01:00
Path : path . Join ( o . fs . opt . Mountpoint , replaceReservedChars ( path . Join ( o . fs . root , o . remote ) ) ) ,
2018-08-22 22:26:18 +02:00
}
// send it
var response api . AllocateFileResponse
2018-08-02 01:02:35 +02:00
err = o . fs . pacer . CallNoRetry ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = o . fs . apiSrv . CallJSON ( ctx , & opts , & request , & response )
2018-08-02 01:02:35 +02:00
return shouldRetry ( resp , err )
} )
if err != nil {
return err
}
2018-12-30 00:53:18 +01:00
// If the file state is INCOMPLETE and CORRPUT, try to upload a then
2018-08-22 22:26:18 +02: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-30 00:53:18 +01:00
ContentType : "application/octet-stream" ,
2018-08-22 22:26:18 +02: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 21:00:37 +02:00
resp , err = o . fs . apiSrv . CallJSON ( ctx , & opts , nil , & result )
2018-08-22 22:26:18 +02:00
if err != nil {
return err
}
// finally update the meta data
o . hasMetaData = true
2019-01-11 18:17:46 +01:00
o . size = result . Bytes
2018-08-22 22:26:18 +02:00
o . md5 = result . Md5
o . modTime = time . Unix ( result . Modified / 1000 , 0 )
2018-12-30 00:53:18 +01:00
} else {
2019-08-14 19:39:19 +02: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 21:00:37 +02:00
return o . readMetaData ( ctx , true )
2018-08-22 22:26:18 +02:00
}
return nil
2018-08-02 01:02:35 +02:00
}
// Remove an object
2019-06-17 10:34:30 +02:00
func ( o * Object ) Remove ( ctx context . Context ) error {
2018-08-02 01:02:35 +02:00
opts := rest . Opts {
Method : "POST" ,
Path : o . filePath ( ) ,
Parameters : url . Values { } ,
2018-10-13 13:32:46 +02:00
NoResponse : true ,
2018-08-02 01:02:35 +02:00
}
2018-09-07 13:58:18 +02:00
if o . fs . opt . HardDelete {
opts . Parameters . Set ( "rm" , "true" )
} else {
opts . Parameters . Set ( "dl" , "true" )
}
2018-08-02 01:02:35 +02:00
return o . fs . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err := o . fs . srv . CallXML ( ctx , & opts , nil , nil )
2018-08-02 01:02:35 +02:00
return shouldRetry ( resp , err )
} )
}
// Check the interfaces are satisfied
var (
2018-09-04 21:02:35 +02: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 )
_ fs . Object = ( * Object ) ( nil )
_ fs . MimeTyper = ( * Object ) ( nil )
2018-08-02 01:02:35 +02:00
)