Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Define a new Features() method for Fs
Optional interfaces are becoming more important in rclone,
--track-renames and --backup-dir both rely on them.

Up to this point rclone has used interface upgrades to define optional
behaviour on Fs objects.  However when one Fs object wraps another it
is very difficult for this scheme to work accurately.  rclone has
relied on specific error messages being returned when the interface
isn't supported - this is unsatisfactory because it means you have to
call the interface to see whether it is supported.

This change enables accurate detection of optional interfaces by use
of a Features struct as returned by an obligatory Fs.Features()
method.  The Features struct contains flags and function pointers
which can be tested against nil to see whether they can be used.

As a result crypt and hubic can accurately reflect the capabilities of
the underlying Fs they are wrapping.
  • Loading branch information
ncw committed Jan 16, 2017
1 parent 3745c52 commit 1fa258c
Show file tree
Hide file tree
Showing 19 changed files with 386 additions and 129 deletions.
7 changes: 7 additions & 0 deletions amazonclouddrive/amazonclouddrive.go
Expand Up @@ -90,6 +90,7 @@ func init() {
// Fs represents a remote acd server
type Fs struct {
name string // name of this remote
features *fs.Features // optional features
c *acd.Client // the connection to the acd server
noAuthClient *http.Client // unauthenticated http client
root string // the path we are working on
Expand Down Expand Up @@ -126,6 +127,11 @@ func (f *Fs) String() string {
return fmt.Sprintf("amazon drive root '%s'", f.root)
}

// Features returns the optional features of this Fs
func (f *Fs) Features() *fs.Features {
return f.features
}

// Pattern to match a acd path
var matcher = regexp.MustCompile(`^([^/]*)(.*)$`)

Expand Down Expand Up @@ -184,6 +190,7 @@ func NewFs(name, root string) (fs.Fs, error) {
noAuthClient: fs.Config.Client(),
ts: ts,
}
f.features = (&fs.Features{CaseInsensitive: true, ReadMimeType: true}).Fill(f)

// Update endpoints
var resp *http.Response
Expand Down
9 changes: 8 additions & 1 deletion b2/b2.go
Expand Up @@ -79,14 +79,15 @@ func init() {
// Fs represents a remote b2 server
type Fs struct {
name string // name of this remote
root string // the path we are working on if any
features *fs.Features // optional features
account string // account name
key string // auth key
endpoint string // name of the starting api endpoint
srv *rest.Client // the connection to the b2 server
bucket string // the bucket we are working on
bucketIDMutex sync.Mutex // mutex to protect _bucketID
_bucketID string // the ID of the bucket we are working on
root string // the path we are working on if any
info api.AuthorizeAccountResponse // result of authorize call
uploadMu sync.Mutex // lock for upload variable
uploads []*api.GetUploadURLResponse // result of get upload URL calls
Expand Down Expand Up @@ -130,6 +131,11 @@ func (f *Fs) String() string {
return fmt.Sprintf("B2 bucket %s path %s", f.bucket, f.root)
}

// Features returns the optional features of this Fs
func (f *Fs) Features() *fs.Features {
return f.features
}

// Pattern to match a b2 path
var matcher = regexp.MustCompile(`^([^/]*)(.*)$`)

Expand Down Expand Up @@ -250,6 +256,7 @@ func NewFs(name, root string) (fs.Fs, error) {
uploadTokens: make(chan struct{}, fs.Config.Transfers),
extraTokens: make(chan struct{}, fs.Config.Transfers),
}
f.features = (&fs.Features{ReadMimeType: true, WriteMimeType: true}).Fill(f)
// Set the test flag if required
if *b2TestMode != "" {
testMode := strings.TrimSpace(*b2TestMode)
Expand Down
103 changes: 75 additions & 28 deletions crypt/crypt.go
Expand Up @@ -88,21 +88,30 @@ func NewFs(name, rpath string) (fs.Fs, error) {
}
f := &Fs{
Fs: wrappedFs,
cipher: cipher,
mode: mode,
name: name,
root: rpath,
cipher: cipher,
mode: mode,
}
// the features here are ones we could support, and they are
// ANDed with the ones from wrappedFs
f.features = (&fs.Features{
CaseInsensitive: mode == NameEncryptionOff,
DuplicateFiles: true,
ReadMimeType: false, // MimeTypes not supported with crypt
WriteMimeType: false,
}).Fill(f).Mask(wrappedFs)
return f, err
}

// Fs represents a wrapped fs.Fs
type Fs struct {
fs.Fs
cipher Cipher
mode NameEncryptionMode
name string
root string
name string
root string
features *fs.Features // optional features
cipher Cipher
mode NameEncryptionMode
}

// Name of the remote (as passed into NewFs)
Expand All @@ -115,6 +124,11 @@ func (f *Fs) Root() string {
return f.root
}

// Features returns the optional features of this Fs
func (f *Fs) Features() *fs.Features {
return f.features
}

// String returns a description of the FS
func (f *Fs) String() string {
return fmt.Sprintf("Encrypted %s", f.Fs.String())
Expand Down Expand Up @@ -177,11 +191,11 @@ func (f *Fs) Rmdir(dir string) error {
//
// Return an error if it doesn't exist
func (f *Fs) Purge() error {
do, ok := f.Fs.(fs.Purger)
if !ok {
do := f.Fs.Features().Purge
if do == nil {
return fs.ErrorCantPurge
}
return do.Purge()
return do()
}

// Copy src to this remote using server side copy operations.
Expand All @@ -194,15 +208,15 @@ func (f *Fs) Purge() error {
//
// If it isn't possible then return fs.ErrorCantCopy
func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
do, ok := f.Fs.(fs.Copier)
if !ok {
do := f.Fs.Features().Copy
if do == nil {
return nil, fs.ErrorCantCopy
}
o, ok := src.(*Object)
if !ok {
return nil, fs.ErrorCantCopy
}
oResult, err := do.Copy(o.Object, f.cipher.EncryptFileName(remote))
oResult, err := do(o.Object, f.cipher.EncryptFileName(remote))
if err != nil {
return nil, err
}
Expand All @@ -219,15 +233,15 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
//
// If it isn't possible then return fs.ErrorCantMove
func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) {
do, ok := f.Fs.(fs.Mover)
if !ok {
do := f.Fs.Features().Move
if do == nil {
return nil, fs.ErrorCantMove
}
o, ok := src.(*Object)
if !ok {
return nil, fs.ErrorCantMove
}
oResult, err := do.Move(o.Object, f.cipher.EncryptFileName(remote))
oResult, err := do(o.Object, f.cipher.EncryptFileName(remote))
if err != nil {
return nil, err
}
Expand All @@ -243,16 +257,48 @@ func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) {
//
// If destination exists then return fs.ErrorDirExists
func (f *Fs) DirMove(src fs.Fs) error {
do, ok := f.Fs.(fs.DirMover)
if !ok {
do := f.Fs.Features().DirMove
if do == nil {
return fs.ErrorCantDirMove
}
srcFs, ok := src.(*Fs)
if !ok {
fs.Debug(srcFs, "Can't move directory - not same remote type")
return fs.ErrorCantDirMove
}
return do.DirMove(srcFs.Fs)
return do(srcFs.Fs)
}

// PutUnchecked uploads the object
//
// This will create a duplicate if we upload a new file without
// checking to see if there is one already - use Put() for that.
func (f *Fs) PutUnchecked(in io.Reader, src fs.ObjectInfo) (fs.Object, error) {
do := f.Fs.Features().PutUnchecked
if do == nil {
return nil, errors.New("can't PutUnchecked")
}
wrappedIn, err := f.cipher.EncryptData(in)
if err != nil {
return nil, err
}
o, err := do(wrappedIn, f.newObjectInfo(src))
if err != nil {
return nil, err
}
return f.newObject(o), nil
}

// CleanUp the trash in the Fs
//
// Implement this if you have a way of emptying the trash or
// otherwise cleaning up old versions of files.
func (f *Fs) CleanUp() error {
do := f.Fs.Features().CleanUp
if do == nil {
return errors.New("can't CleanUp")
}
return do()
}

// UnWrap returns the Fs that this Fs is wrapping
Expand Down Expand Up @@ -473,14 +519,15 @@ func (lo *ListOpts) IncludeDirectory(remote string) bool {

// Check the interfaces are satisfied
var (
_ fs.Fs = (*Fs)(nil)
_ fs.Purger = (*Fs)(nil)
_ fs.Copier = (*Fs)(nil)
_ fs.Mover = (*Fs)(nil)
_ fs.DirMover = (*Fs)(nil)
// _ fs.PutUncheckeder = (*Fs)(nil)
_ fs.UnWrapper = (*Fs)(nil)
_ fs.ObjectInfo = (*ObjectInfo)(nil)
_ fs.Object = (*Object)(nil)
_ fs.ListOpts = (*ListOpts)(nil)
_ fs.Fs = (*Fs)(nil)
_ fs.Purger = (*Fs)(nil)
_ fs.Copier = (*Fs)(nil)
_ fs.Mover = (*Fs)(nil)
_ fs.DirMover = (*Fs)(nil)
_ fs.PutUncheckeder = (*Fs)(nil)
_ fs.CleanUpper = (*Fs)(nil)
_ fs.UnWrapper = (*Fs)(nil)
_ fs.ObjectInfo = (*ObjectInfo)(nil)
_ fs.Object = (*Object)(nil)
_ fs.ListOpts = (*ListOpts)(nil)
)
9 changes: 8 additions & 1 deletion drive/drive.go
Expand Up @@ -115,8 +115,9 @@ func init() {
// Fs represents a remote drive server
type Fs struct {
name string // name of this remote
svc *drive.Service // the connection to the drive server
root string // the path we are working on
features *fs.Features // optional features
svc *drive.Service // the connection to the drive server
client *http.Client // authorized client
about *drive.About // information about the drive, including the root
dirCache *dircache.DirCache // Map of directory path to directory id
Expand Down Expand Up @@ -154,6 +155,11 @@ func (f *Fs) String() string {
return fmt.Sprintf("Google drive root '%s'", f.root)
}

// Features returns the optional features of this Fs
func (f *Fs) Features() *fs.Features {
return f.features
}

// shouldRetry determines whehter a given err rates being retried
func shouldRetry(err error) (again bool, errOut error) {
again = false
Expand Down Expand Up @@ -294,6 +300,7 @@ func NewFs(name, path string) (fs.Fs, error) {
root: root,
pacer: pacer.New().SetMinSleep(minSleep).SetPacer(pacer.GoogleDrivePacer),
}
f.features = (&fs.Features{DuplicateFiles: true, ReadMimeType: true, WriteMimeType: true}).Fill(f)

// Create a new authorized Drive client.
f.client = oAuthClient
Expand Down
9 changes: 8 additions & 1 deletion dropbox/dropbox.go
Expand Up @@ -96,8 +96,9 @@ func configHelper(name string) {
// Fs represents a remote dropbox server
type Fs struct {
name string // name of this remote
db *dropbox.Dropbox // the connection to the dropbox server
root string // the path we are working on
features *fs.Features // optional features
db *dropbox.Dropbox // the connection to the dropbox server
slashRoot string // root with "/" prefix, lowercase
slashRootSlash string // root with "/" prefix and postfix, lowercase
}
Expand Down Expand Up @@ -129,6 +130,11 @@ func (f *Fs) String() string {
return fmt.Sprintf("Dropbox root '%s'", f.root)
}

// Features returns the optional features of this Fs
func (f *Fs) Features() *fs.Features {
return f.features
}

// Makes a new dropbox from the config
func newDropbox(name string) (*dropbox.Dropbox, error) {
db := dropbox.NewDropbox()
Expand Down Expand Up @@ -159,6 +165,7 @@ func NewFs(name, root string) (fs.Fs, error) {
name: name,
db: db,
}
f.features = (&fs.Features{CaseInsensitive: true, ReadMimeType: true}).Fill(f)
f.setRoot(root)

// Read the token from the config file
Expand Down

0 comments on commit 1fa258c

Please sign in to comment.