2018-08-02 01:02:35 +02:00
package jottacloud
import (
2018-08-14 23:15:21 +02:00
"bytes"
"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"
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/ncw/rclone/backend/jottacloud/api"
"github.com/ncw/rclone/fs"
2018-08-20 16:38:21 +02:00
"github.com/ncw/rclone/fs/accounting"
2018-12-30 20:22:33 +01:00
"github.com/ncw/rclone/fs/config"
2018-08-02 01:02:35 +02:00
"github.com/ncw/rclone/fs/config/configmap"
"github.com/ncw/rclone/fs/config/configstruct"
"github.com/ncw/rclone/fs/config/obscure"
"github.com/ncw/rclone/fs/fserrors"
"github.com/ncw/rclone/fs/fshttp"
"github.com/ncw/rclone/fs/hash"
2018-09-06 15:13:38 +02:00
"github.com/ncw/rclone/fs/walk"
2018-08-22 22:26:18 +02:00
"github.com/ncw/rclone/lib/oauthutil"
2018-08-02 01:02:35 +02:00
"github.com/ncw/rclone/lib/pacer"
"github.com/ncw/rclone/lib/rest"
"github.com/pkg/errors"
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-01-11 17:55:25 +01:00
defaultMountpoint = "Sync" // nolint
2018-08-22 22:26:18 +02:00
rootURL = "https://www.jottacloud.com/jfs/"
apiURL = "https://api.jottacloud.com/files/v1/"
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"
cachePrefix = "rclone-jcmd5-"
rcloneClientID = "nibfk8biu12ju7hpqomr8b1e40"
rcloneEncryptedClientSecret = "Vp8eAv7eVElMnQwN-kgU9cbhgApNDaMqWdlDi5qFydlQoji4JBxrGMF2"
configUsername = "user"
)
var (
// Description of how to auth for this app for a personal account
oauthConfig = & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
AuthURL : tokenURL ,
TokenURL : tokenURL ,
} ,
ClientID : rcloneClientID ,
ClientSecret : obscure . MustReveal ( rcloneEncryptedClientSecret ) ,
RedirectURL : oauthutil . RedirectLocalhostURL ,
}
2018-08-02 01:02:35 +02:00
)
// Register with Fs
func init ( ) {
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 ) {
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
}
}
2018-08-22 22:26:18 +02:00
username , ok := m . Get ( configUsername )
if ! ok {
2019-01-28 21:50:51 +01:00
log . Fatalf ( "No username defined" )
2018-08-22 22:26:18 +02:00
}
2018-12-30 20:22:33 +01:00
password := config . GetPassword ( "Your Jottacloud password is only required during config and will not be stored." )
2018-08-22 22:26:18 +02:00
2019-01-28 21:50:51 +01:00
// prepare out token request with username and password
2018-12-30 20:22:33 +01:00
srv := rest . NewClient ( fshttp . NewClient ( fs . Config ) )
2018-08-22 22:26:18 +02:00
values := url . Values { }
values . Set ( "grant_type" , "PASSWORD" )
values . Set ( "password" , password )
values . Set ( "username" , username )
2018-12-30 20:22:33 +01:00
values . Set ( "client_id" , oauthConfig . ClientID )
values . Set ( "client_secret" , oauthConfig . ClientSecret )
2018-08-22 22:26:18 +02:00
opts := rest . Opts {
Method : "POST" ,
RootURL : oauthConfig . Endpoint . AuthURL ,
ContentType : "application/x-www-form-urlencoded" ,
2018-12-30 00:53:18 +01:00
Parameters : values ,
2018-08-22 22:26:18 +02:00
}
2018-12-30 00:53:18 +01:00
var jsonToken api . TokenJSON
2018-08-22 22:26:18 +02:00
resp , err := srv . CallJSON ( & opts , nil , & jsonToken )
if err != nil {
2019-01-28 21:50:51 +01:00
// if 2fa is enabled the first request is expected to fail. we'lls 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 has 2 factor authentication enabled you will receive a verification code via SMS.\n" )
fmt . Printf ( "Enter verification code> " )
authCode := config . ReadLine ( )
authCode = strings . Replace ( authCode , "-" , "" , - 1 ) // the sms received contains a pair of 3 digit numbers seperated by '-' but wants a single 6 digit number
opts . ExtraHeaders = make ( map [ string ] string )
opts . ExtraHeaders [ "X-Jottacloud-Otp" ] = authCode
resp , err = srv . CallJSON ( & opts , nil , & jsonToken )
}
}
if err != nil {
log . Fatalf ( "Failed to get resource token: %v" , err )
}
2018-08-22 22:26:18 +02:00
}
var token oauth2 . Token
token . AccessToken = jsonToken . AccessToken
token . RefreshToken = jsonToken . RefreshToken
token . TokenType = jsonToken . TokenType
token . Expiry = time . Now ( ) . Add ( time . Duration ( jsonToken . ExpiresIn ) * time . Second )
// finally save them in the config
err = oauthutil . PutToken ( name , m , & token , true )
if err != nil {
2019-01-28 21:50:51 +01:00
log . Fatalf ( "Error while setting token: %s" , err )
2018-08-22 22:26:18 +02:00
}
} ,
2018-08-02 01:02:35 +02:00
Options : [ ] fs . Option { {
2018-08-22 22:26:18 +02:00
Name : configUsername ,
2018-12-30 20:22:33 +01:00
Help : "User Name:" ,
2018-08-02 01:02:35 +02:00
} , {
Name : "mountpoint" ,
Help : "The mountpoint to use." ,
Required : true ,
Examples : [ ] fs . OptionExample { {
Value : "Sync" ,
Help : "Will be synced by the official client." ,
} , {
Value : "Archive" ,
Help : "Archive" ,
} } ,
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" ,
Help : "Files bigger than this can be resumed if the upload failes." ,
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 {
2018-08-14 23:15:21 +02:00
User string ` config:"user" `
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
pacer * pacer . Pacer
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
}
// readMetaDataForPath reads the metadata from the path
func ( f * Fs ) readMetaDataForPath ( path string ) ( info * api . JottaFile , err error ) {
opts := rest . Opts {
Method : "GET" ,
Path : f . filePath ( path ) ,
}
var result api . JottaFile
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
resp , err = f . srv . CallXML ( & opts , nil , & result )
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-15 01:12:20 +02:00
// getAccountInfo retrieves account information
func ( f * Fs ) getAccountInfo ( ) ( info * api . AccountInfo , err error ) {
2018-08-02 01:02:35 +02:00
opts := rest . Opts {
Method : "GET" ,
2018-09-16 22:30:20 +02:00
Path : urlPathEscape ( f . user ) ,
2018-08-02 01:02:35 +02:00
}
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2018-08-15 01:12:20 +02:00
resp , err = f . srv . CallXML ( & opts , nil , & info )
2018-08-02 01:02:35 +02:00
return shouldRetry ( resp , err )
} )
if err != nil {
2018-08-15 01:12:20 +02:00
return nil , err
2018-08-02 01:02:35 +02:00
}
2018-08-15 01:12:20 +02:00
return info , nil
}
// setEndpointUrl reads the account id and generates the API endpoint URL
func ( f * Fs ) setEndpointURL ( mountpoint string ) ( err error ) {
info , err := f . getAccountInfo ( )
if err != nil {
return errors . Wrap ( err , "failed to get endpoint url" )
}
2018-09-16 22:30:20 +02:00
f . endpointURL = urlPathEscape ( path . Join ( info . Username , defaultDevice , mountpoint ) )
2018-08-02 01:02:35 +02:00
return nil
}
// 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
}
// filePath returns a escaped file path (f.root, remote)
func ( o * Object ) filePath ( ) string {
return o . fs . filePath ( o . remote )
}
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 ( )
// make the refesh 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())
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 ) {
// 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 )
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 ,
user : opt . User ,
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 ) ,
pacer : pacer . New ( ) . SetMinSleep ( minSleep ) . SetMaxSleep ( maxSleep ) . SetDecayConstant ( 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 {
_ , err := f . readMetaDataForPath ( "" )
return err
} )
2018-08-15 01:12:20 +02:00
err = f . setEndpointURL ( opt . Mountpoint )
2018-08-02 01:02:35 +02:00
if err != nil {
return nil , errors . Wrap ( err , "couldn't get account info" )
}
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 = ""
}
_ , err := f . NewObject ( remote )
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.
func ( f * Fs ) newObjectWithInfo ( remote string , info * api . JottaFile ) ( fs . Object , error ) {
o := & Object {
fs : f ,
remote : remote ,
}
var err error
if info != nil {
// Set info
err = o . setMetaData ( info )
} else {
2018-12-30 00:53:18 +01:00
err = o . readMetaData ( 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.
func ( f * Fs ) NewObject ( remote string ) ( fs . Object , error ) {
return f . newObjectWithInfo ( remote , nil )
}
// CreateDir makes a directory
func ( f * Fs ) CreateDir ( path string ) ( jf * api . JottaFolder , err error ) {
// 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 ) {
resp , err = f . srv . CallXML ( & opts , nil , & jf )
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.
func ( f * Fs ) List ( dir string ) ( entries fs . DirEntries , err error ) {
2018-12-30 00:53:18 +01:00
//fmt.Printf("List: %s\n", f.filePath(dir))
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 ) {
resp , err = f . srv . CallXML ( & opts , nil , & result )
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 ) )
o , err := f . newObjectWithInfo ( remote , item )
if err != nil {
continue
}
entries = append ( entries , o )
}
//fmt.Printf("Entries: %+v\n", entries)
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
2018-09-09 00:12:47 +02:00
func ( f * Fs ) listFileDir ( remoteStartPath string , startFolder * api . JottaFolder , fn listFileDirFn ) error {
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 ) )
o , err := f . newObjectWithInfo ( remoteFile , file )
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.
//
// This should return ErrDirNotFound if the directory isn't
// found.
//
// It should call callback for each tranche of entries read.
// These need not be returned in any particular order. If
// callback returns an error then the listing will stop
// immediately.
//
// Don't implement this unless you have a more efficient way
// of listing recursively that doing a directory traversal.
func ( f * Fs ) ListR ( dir string , callback fs . ListRCallback ) ( err error ) {
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 ) {
resp , err = f . srv . CallXML ( & opts , nil , & result )
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 )
2018-09-09 00:12:47 +02:00
err = f . listFileDir ( 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
func ( f * Fs ) Put ( in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
o := f . createObject ( src . Remote ( ) , src . ModTime ( ) , src . Size ( ) )
return o , o . Update ( in , src , options ... )
}
// mkParentDir makes the parent of the native path dirPath if
// necessary and any directories above that
func ( f * Fs ) mkParentDir ( dirPath string ) error {
// 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 = ""
}
return f . Mkdir ( parent )
}
// Mkdir creates the container if it doesn't exist
func ( f * Fs ) Mkdir ( dir string ) error {
_ , err := f . CreateDir ( dir )
return err
}
// purgeCheck removes the root directory, if check is set then it
// refuses to do so if it has anything in
func ( f * Fs ) purgeCheck ( dir string , check bool ) ( err error ) {
root := path . Join ( f . root , dir )
if root == "" {
return errors . New ( "can't purge root directory" )
}
// check that the directory exists
entries , err := f . List ( dir )
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 ) {
resp , err = f . srv . Call ( & opts )
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
}
// TODO: Parse response?
return nil
}
// Rmdir deletes the root folder
//
// Returns an error if it isn't empty
func ( f * Fs ) Rmdir ( dir string ) error {
return f . purgeCheck ( dir , true )
}
// Precision return the precision of this Fs
func ( f * Fs ) Precision ( ) time . Duration {
return time . Second
}
// Purge deletes all the files and the container
//
// Optional interface: Only implement this if you have a way of
// deleting all the files quicker than just running Remove() on the
// result of List()
func ( f * Fs ) Purge ( ) error {
return f . purgeCheck ( "" , false )
}
// copyOrMoves copys or moves directories or files depending on the mthod parameter
func ( f * Fs ) copyOrMove ( method , src , dest string ) ( info * api . JottaFile , err error ) {
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 ) {
resp , err = f . srv . CallXML ( & opts , nil , & info )
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
func ( f * Fs ) Copy ( src fs . Object , remote string ) ( fs . Object , error ) {
srcObj , ok := src . ( * Object )
if ! ok {
fs . Debugf ( src , "Can't copy - not same remote type" )
return nil , fs . ErrorCantMove
}
err := f . mkParentDir ( remote )
if err != nil {
return nil , err
}
info , err := f . copyOrMove ( "cp" , srcObj . filePath ( ) , remote )
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
}
return f . newObjectWithInfo ( remote , info )
//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
func ( f * Fs ) Move ( src fs . Object , remote string ) ( fs . Object , error ) {
srcObj , ok := src . ( * Object )
if ! ok {
fs . Debugf ( src , "Can't move - not same remote type" )
return nil , fs . ErrorCantMove
}
err := f . mkParentDir ( remote )
if err != nil {
return nil , err
}
info , err := f . copyOrMove ( "mv" , srcObj . filePath ( ) , remote )
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
}
return f . newObjectWithInfo ( remote , info )
//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
func ( f * Fs ) DirMove ( src fs . Fs , srcRemote , dstRemote string ) error {
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
_ , err = f . List ( dstRemote )
if err == fs . ErrorDirNotFound {
// OK
} else if err != nil {
return err
} else {
return fs . ErrorDirExists
}
_ , err = f . copyOrMove ( "mvDir" , path . Join ( f . endpointURL , replaceReservedChars ( srcPath ) ) + "/" , dstRemote )
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)
func ( f * Fs ) PublicLink ( remote string ) ( link string , err error ) {
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 ) {
resp , err = f . srv . CallXML ( & opts , nil , & result )
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
func ( f * Fs ) About ( ) ( * fs . Usage , error ) {
info , err := f . getAccountInfo ( )
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
}
// Hash returns the MD5 of an object returning a lowercase hex string
func ( o * Object ) Hash ( t hash . Type ) ( string , error ) {
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 {
2018-12-30 00:53:18 +01:00
err := o . readMetaData ( 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
func ( o * Object ) MimeType ( ) string {
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
o . size = int64 ( info . Size )
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
}
2018-12-30 00:53:18 +01:00
func ( o * Object ) readMetaData ( force bool ) ( err error ) {
if o . hasMetaData && ! force {
2018-08-02 01:02:35 +02:00
return nil
}
info , err := o . fs . readMetaDataForPath ( o . remote )
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
func ( o * Object ) ModTime ( ) time . Time {
2018-12-30 00:53:18 +01:00
err := o . readMetaData ( 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
func ( o * Object ) SetModTime ( modTime time . Time ) error {
return fs . ErrorCantSetModTime
}
// Storable returns a boolean showing whether this object storable
func ( o * Object ) Storable ( ) bool {
return true
}
// Open an object for read
func ( o * Object ) Open ( options ... fs . OpenOption ) ( in io . ReadCloser , err error ) {
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 ) {
resp , err = o . fs . srv . Call ( & opts )
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 ( )
// use the teeReader to write to the local file AND caclulate the MD5 while doing so
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
func ( o * Object ) Update ( in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( err error ) {
2018-08-20 16:38:21 +02:00
size := src . Size ( )
2018-08-14 23:15:21 +02:00
md5String , err := src . Hash ( hash . MD5 )
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" ,
Path : "allocate" ,
ExtraHeaders : make ( map [ string ] string ) ,
}
fileDate := api . Time ( src . ModTime ( ) ) . 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 ) {
2018-08-22 22:26:18 +02:00
resp , err = o . fs . apiSrv . CallJSON ( & 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
resp , err = o . fs . apiSrv . CallJSON ( & opts , nil , & result )
if err != nil {
return err
}
// finally update the meta data
o . hasMetaData = true
o . size = int64 ( result . Bytes )
o . md5 = result . Md5
o . modTime = time . Unix ( result . Modified / 1000 , 0 )
2018-12-30 00:53:18 +01:00
} else {
// If the file state is COMPLETE we don't need to upload it because the file was allready found but we still ned to update our metadata
2018-12-30 20:22:33 +01:00
return o . readMetaData ( true )
2018-08-22 22:26:18 +02:00
}
return nil
2018-08-02 01:02:35 +02:00
}
// Remove an object
func ( o * Object ) Remove ( ) error {
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 ) {
resp , err := o . fs . srv . CallXML ( & opts , nil , nil )
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
)