2016-07-25 18:18:56 +00:00
|
|
|
// Package crypt provides wrappers for Fs and Object which implement encryption
|
|
|
|
package crypt
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2016-09-10 10:29:57 +00:00
|
|
|
"io/ioutil"
|
2016-07-25 18:18:56 +00:00
|
|
|
"path"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/ncw/rclone/fs"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Register with Fs
|
|
|
|
func init() {
|
|
|
|
fs.Register(&fs.RegInfo{
|
|
|
|
Name: "crypt",
|
|
|
|
Description: "Encrypt/Decrypt a remote",
|
|
|
|
NewFs: NewFs,
|
|
|
|
Options: []fs.Option{{
|
|
|
|
Name: "remote",
|
|
|
|
Help: "Remote to encrypt/decrypt.",
|
|
|
|
}, {
|
2016-08-20 17:46:10 +00:00
|
|
|
Name: "filename_encryption",
|
|
|
|
Help: "How to encrypt the filenames.",
|
2016-07-25 18:18:56 +00:00
|
|
|
Examples: []fs.OptionExample{
|
|
|
|
{
|
2016-08-20 17:46:10 +00:00
|
|
|
Value: "off",
|
|
|
|
Help: "Don't encrypt the file names. Adds a \".bin\" extension only.",
|
2016-07-25 18:18:56 +00:00
|
|
|
}, {
|
2016-08-20 17:46:10 +00:00
|
|
|
Value: "standard",
|
|
|
|
Help: "Encrypt the filenames see the docs for the details.",
|
2016-07-25 18:18:56 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "password",
|
|
|
|
Help: "Password or pass phrase for encryption.",
|
|
|
|
IsPassword: true,
|
2016-08-19 19:02:02 +00:00
|
|
|
}, {
|
|
|
|
Name: "password2",
|
|
|
|
Help: "Password or pass phrase for salt. Optional but recommended.\nShould be different to the previous password.",
|
|
|
|
IsPassword: true,
|
|
|
|
Optional: true,
|
2016-07-25 18:18:56 +00:00
|
|
|
}},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewFs contstructs an Fs from the path, container:path
|
|
|
|
func NewFs(name, rpath string) (fs.Fs, error) {
|
2016-08-20 17:46:10 +00:00
|
|
|
mode, err := NewNameEncryptionMode(fs.ConfigFile.MustValue(name, "filename_encryption", "standard"))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-07-25 18:18:56 +00:00
|
|
|
password := fs.ConfigFile.MustValue(name, "password", "")
|
|
|
|
if password == "" {
|
|
|
|
return nil, errors.New("password not set in config file")
|
|
|
|
}
|
2016-08-20 17:46:10 +00:00
|
|
|
password, err = fs.Reveal(password)
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to decrypt password")
|
|
|
|
}
|
2016-08-19 19:02:02 +00:00
|
|
|
salt := fs.ConfigFile.MustValue(name, "password2", "")
|
|
|
|
if salt != "" {
|
|
|
|
salt, err = fs.Reveal(salt)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to decrypt password2")
|
|
|
|
}
|
|
|
|
}
|
2016-08-20 17:46:10 +00:00
|
|
|
cipher, err := newCipher(mode, password, salt)
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to make cipher")
|
|
|
|
}
|
|
|
|
remote := fs.ConfigFile.MustValue(name, "remote")
|
2016-08-20 17:46:10 +00:00
|
|
|
// Look for a file first
|
|
|
|
remotePath := path.Join(remote, cipher.EncryptFileName(rpath))
|
2016-07-25 18:18:56 +00:00
|
|
|
wrappedFs, err := fs.NewFs(remotePath)
|
2016-08-20 17:46:10 +00:00
|
|
|
// if that didn't produce a file, look for a directory
|
|
|
|
if err != fs.ErrorIsFile {
|
|
|
|
remotePath = path.Join(remote, cipher.EncryptDirName(rpath))
|
|
|
|
wrappedFs, err = fs.NewFs(remotePath)
|
|
|
|
}
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != fs.ErrorIsFile && err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "failed to make remote %q to wrap", remotePath)
|
|
|
|
}
|
|
|
|
f := &Fs{
|
2016-08-20 17:46:10 +00:00
|
|
|
Fs: wrappedFs,
|
|
|
|
cipher: cipher,
|
|
|
|
mode: mode,
|
2016-09-09 07:38:18 +00:00
|
|
|
name: name,
|
|
|
|
root: rpath,
|
2016-07-25 18:18:56 +00:00
|
|
|
}
|
|
|
|
return f, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fs represents a wrapped fs.Fs
|
|
|
|
type Fs struct {
|
|
|
|
fs.Fs
|
2016-08-20 17:46:10 +00:00
|
|
|
cipher Cipher
|
|
|
|
mode NameEncryptionMode
|
2016-09-09 07:38:18 +00:00
|
|
|
name string
|
|
|
|
root string
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2016-07-25 18:18:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// String returns a description of the FS
|
|
|
|
func (f *Fs) String() string {
|
2016-08-23 16:43:43 +00:00
|
|
|
return fmt.Sprintf("Encrypted %s", f.Fs.String())
|
2016-07-25 18:18:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// List the Fs into a channel
|
|
|
|
func (f *Fs) List(opts fs.ListOpts, dir string) {
|
2016-08-20 17:46:10 +00:00
|
|
|
f.Fs.List(f.newListOpts(opts, dir), f.cipher.EncryptDirName(dir))
|
2016-07-25 18:18:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewObject finds the Object at remote.
|
|
|
|
func (f *Fs) NewObject(remote string) (fs.Object, error) {
|
2016-08-20 17:46:10 +00:00
|
|
|
o, err := f.Fs.NewObject(f.cipher.EncryptFileName(remote))
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return f.newObject(o), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Put in to the remote path with the modTime given of the given size
|
|
|
|
//
|
|
|
|
// May create the object even if it returns an error - if so
|
|
|
|
// will return the object and the error, otherwise will return
|
|
|
|
// nil and the error
|
|
|
|
func (f *Fs) Put(in io.Reader, src fs.ObjectInfo) (fs.Object, error) {
|
|
|
|
wrappedIn, err := f.cipher.EncryptData(in)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
o, err := f.Fs.Put(wrappedIn, f.newObjectInfo(src))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return f.newObject(o), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hashes returns the supported hash sets.
|
|
|
|
func (f *Fs) Hashes() fs.HashSet {
|
|
|
|
return fs.HashSet(fs.HashNone)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Purge all files in the root and the root directory
|
|
|
|
//
|
|
|
|
// Implement this if you have a way of deleting all the files
|
|
|
|
// quicker than just running Remove() on the result of List()
|
|
|
|
//
|
|
|
|
// Return an error if it doesn't exist
|
|
|
|
func (f *Fs) Purge() error {
|
|
|
|
do, ok := f.Fs.(fs.Purger)
|
|
|
|
if !ok {
|
|
|
|
return fs.ErrorCantPurge
|
|
|
|
}
|
|
|
|
return do.Purge()
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
do, ok := f.Fs.(fs.Copier)
|
|
|
|
if !ok {
|
|
|
|
return nil, fs.ErrorCantCopy
|
|
|
|
}
|
|
|
|
o, ok := src.(*Object)
|
|
|
|
if !ok {
|
|
|
|
return nil, fs.ErrorCantCopy
|
|
|
|
}
|
2016-08-20 17:46:10 +00:00
|
|
|
oResult, err := do.Copy(o.Object, f.cipher.EncryptFileName(remote))
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return f.newObject(oResult), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
do, ok := f.Fs.(fs.Mover)
|
|
|
|
if !ok {
|
|
|
|
return nil, fs.ErrorCantMove
|
|
|
|
}
|
|
|
|
o, ok := src.(*Object)
|
|
|
|
if !ok {
|
2016-08-23 16:43:43 +00:00
|
|
|
return nil, fs.ErrorCantMove
|
2016-07-25 18:18:56 +00:00
|
|
|
}
|
2016-08-20 17:46:10 +00:00
|
|
|
oResult, err := do.Move(o.Object, f.cipher.EncryptFileName(remote))
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return f.newObject(oResult), nil
|
|
|
|
}
|
|
|
|
|
2016-08-23 16:43:43 +00:00
|
|
|
// DirMove moves src to this remote 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) error {
|
|
|
|
do, ok := f.Fs.(fs.DirMover)
|
|
|
|
if !ok {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2016-07-25 18:18:56 +00:00
|
|
|
// UnWrap returns the Fs that this Fs is wrapping
|
|
|
|
func (f *Fs) UnWrap() fs.Fs {
|
|
|
|
return f.Fs
|
|
|
|
}
|
|
|
|
|
|
|
|
// Object describes a wrapped for being read from the Fs
|
|
|
|
//
|
|
|
|
// This decrypts the remote name and decrypts the data
|
|
|
|
type Object struct {
|
|
|
|
fs.Object
|
|
|
|
f *Fs
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Fs) newObject(o fs.Object) *Object {
|
|
|
|
return &Object{
|
|
|
|
Object: o,
|
|
|
|
f: f,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fs returns read only access to the Fs that this object is part of
|
|
|
|
func (o *Object) Fs() fs.Info {
|
|
|
|
return o.f
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
remote := o.Object.Remote()
|
2016-08-20 17:46:10 +00:00
|
|
|
decryptedName, err := o.f.cipher.DecryptFileName(remote)
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
fs.Debug(remote, "Undecryptable file name: %v", err)
|
|
|
|
return remote
|
|
|
|
}
|
|
|
|
return decryptedName
|
|
|
|
}
|
|
|
|
|
|
|
|
// Size returns the size of the file
|
|
|
|
func (o *Object) Size() int64 {
|
|
|
|
size, err := o.f.cipher.DecryptedSize(o.Object.Size())
|
|
|
|
if err != nil {
|
|
|
|
fs.Debug(o, "Bad size for decrypt: %v", err)
|
|
|
|
}
|
|
|
|
return size
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hash returns the selected checksum of the file
|
|
|
|
// If no checksum is available it returns ""
|
|
|
|
func (o *Object) Hash(hash fs.HashType) (string, error) {
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open opens the file for read. Call Close() on the returned io.ReadCloser
|
2016-09-10 10:29:57 +00:00
|
|
|
func (o *Object) Open(options ...fs.OpenOption) (io.ReadCloser, error) {
|
|
|
|
var offset int64
|
|
|
|
for _, option := range options {
|
|
|
|
switch x := option.(type) {
|
|
|
|
case *fs.SeekOption:
|
|
|
|
offset = x.Offset
|
|
|
|
default:
|
|
|
|
if option.Mandatory() {
|
|
|
|
fs.Log(o, "Unsupported mandatory option: %v", option)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-07-25 18:18:56 +00:00
|
|
|
in, err := o.Object.Open()
|
|
|
|
if err != nil {
|
2016-09-10 10:29:57 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// This reads the header and checks it is OK
|
|
|
|
rc, err := o.f.cipher.DecryptData(in)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If seeking required, then...
|
|
|
|
if offset != 0 {
|
|
|
|
// FIXME could cache the unseeked decrypter as we re-read the header on every seek
|
|
|
|
decrypter := rc.(*decrypter)
|
|
|
|
|
|
|
|
// Seek the decrypter and work out where to seek the
|
|
|
|
// underlying file and how many bytes to discard
|
|
|
|
underlyingOffset, discard := decrypter.seek(offset)
|
|
|
|
|
|
|
|
// Re-open stream with a seek of underlyingOffset
|
|
|
|
err = in.Close()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
in, err := o.Object.Open(&fs.SeekOption{Offset: underlyingOffset})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the stream
|
|
|
|
decrypter.rc = in
|
|
|
|
|
|
|
|
// Discard the bytes
|
|
|
|
_, err = io.CopyN(ioutil.Discard, decrypter, discard)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-07-25 18:18:56 +00:00
|
|
|
}
|
2016-09-10 10:29:57 +00:00
|
|
|
|
|
|
|
return rc, err
|
2016-07-25 18:18:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update in to the object with the modTime given of the given size
|
|
|
|
func (o *Object) Update(in io.Reader, src fs.ObjectInfo) error {
|
|
|
|
wrappedIn, err := o.f.cipher.EncryptData(in)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return o.Object.Update(wrappedIn, o.f.newObjectInfo(src))
|
|
|
|
}
|
|
|
|
|
|
|
|
// newDir returns a dir with the Name decrypted
|
|
|
|
func (f *Fs) newDir(dir *fs.Dir) *fs.Dir {
|
|
|
|
new := *dir
|
|
|
|
remote := dir.Name
|
2016-08-20 17:46:10 +00:00
|
|
|
decryptedRemote, err := f.cipher.DecryptDirName(remote)
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
fs.Debug(remote, "Undecryptable dir name: %v", err)
|
|
|
|
} else {
|
|
|
|
new.Name = decryptedRemote
|
|
|
|
}
|
|
|
|
return &new
|
|
|
|
}
|
|
|
|
|
|
|
|
// ObjectInfo describes a wrapped fs.ObjectInfo for being the source
|
|
|
|
//
|
|
|
|
// This encrypts the remote name and adjusts the size
|
|
|
|
type ObjectInfo struct {
|
|
|
|
fs.ObjectInfo
|
|
|
|
f *Fs
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Fs) newObjectInfo(src fs.ObjectInfo) *ObjectInfo {
|
|
|
|
return &ObjectInfo{
|
|
|
|
ObjectInfo: src,
|
|
|
|
f: f,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fs returns read only access to the Fs that this object is part of
|
|
|
|
func (o *ObjectInfo) Fs() fs.Info {
|
|
|
|
return o.f
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remote returns the remote path
|
|
|
|
func (o *ObjectInfo) Remote() string {
|
2016-08-20 17:46:10 +00:00
|
|
|
return o.f.cipher.EncryptFileName(o.ObjectInfo.Remote())
|
2016-07-25 18:18:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Size returns the size of the file
|
|
|
|
func (o *ObjectInfo) Size() int64 {
|
|
|
|
return o.f.cipher.EncryptedSize(o.ObjectInfo.Size())
|
|
|
|
}
|
|
|
|
|
2016-08-25 20:26:55 +00:00
|
|
|
// Hash returns the selected checksum of the file
|
|
|
|
// If no checksum is available it returns ""
|
|
|
|
func (o *ObjectInfo) Hash(hash fs.HashType) (string, error) {
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
2016-07-25 18:18:56 +00:00
|
|
|
// ListOpts wraps a listopts decrypting the directory listing and
|
|
|
|
// replacing the Objects
|
|
|
|
type ListOpts struct {
|
|
|
|
fs.ListOpts
|
|
|
|
f *Fs
|
|
|
|
dir string // dir we are listing
|
|
|
|
mu sync.Mutex // to protect dirs
|
|
|
|
dirs map[string]struct{} // keep track of synthetic directory objects added
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make a ListOpts wrapper
|
|
|
|
func (f *Fs) newListOpts(lo fs.ListOpts, dir string) *ListOpts {
|
|
|
|
if dir != "" {
|
|
|
|
dir += "/"
|
|
|
|
}
|
|
|
|
return &ListOpts{
|
|
|
|
ListOpts: lo,
|
|
|
|
f: f,
|
|
|
|
dir: dir,
|
|
|
|
dirs: make(map[string]struct{}),
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Level gets the recursion level for this listing.
|
|
|
|
//
|
|
|
|
// Fses may ignore this, but should implement it for improved efficiency if possible.
|
|
|
|
//
|
|
|
|
// Level 1 means list just the contents of the directory
|
|
|
|
//
|
|
|
|
// Each returned item must have less than level `/`s in.
|
|
|
|
func (lo *ListOpts) Level() int {
|
|
|
|
return lo.ListOpts.Level()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add an object to the output.
|
|
|
|
// If the function returns true, the operation has been aborted.
|
|
|
|
// Multiple goroutines can safely add objects concurrently.
|
|
|
|
func (lo *ListOpts) Add(obj fs.Object) (abort bool) {
|
|
|
|
remote := obj.Remote()
|
2016-08-20 17:46:10 +00:00
|
|
|
_, err := lo.f.cipher.DecryptFileName(remote)
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
fs.Debug(remote, "Skipping undecryptable file name: %v", err)
|
|
|
|
return lo.ListOpts.IsFinished()
|
|
|
|
}
|
|
|
|
return lo.ListOpts.Add(lo.f.newObject(obj))
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddDir adds a directory to the output.
|
|
|
|
// If the function returns true, the operation has been aborted.
|
|
|
|
// Multiple goroutines can safely add objects concurrently.
|
|
|
|
func (lo *ListOpts) AddDir(dir *fs.Dir) (abort bool) {
|
|
|
|
remote := dir.Name
|
2016-08-20 17:46:10 +00:00
|
|
|
_, err := lo.f.cipher.DecryptDirName(remote)
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
fs.Debug(remote, "Skipping undecryptable dir name: %v", err)
|
|
|
|
return lo.ListOpts.IsFinished()
|
|
|
|
}
|
|
|
|
return lo.ListOpts.AddDir(lo.f.newDir(dir))
|
|
|
|
}
|
|
|
|
|
|
|
|
// IncludeDirectory returns whether this directory should be
|
|
|
|
// included in the listing (and recursed into or not).
|
|
|
|
func (lo *ListOpts) IncludeDirectory(remote string) bool {
|
2016-08-20 17:46:10 +00:00
|
|
|
decryptedRemote, err := lo.f.cipher.DecryptDirName(remote)
|
2016-07-25 18:18:56 +00:00
|
|
|
if err != nil {
|
|
|
|
fs.Debug(remote, "Not including undecryptable directory name: %v", err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return lo.ListOpts.IncludeDirectory(decryptedRemote)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check the interfaces are satisfied
|
|
|
|
var (
|
2016-08-23 16:43:43 +00:00
|
|
|
_ fs.Fs = (*Fs)(nil)
|
|
|
|
_ fs.Purger = (*Fs)(nil)
|
|
|
|
_ fs.Copier = (*Fs)(nil)
|
|
|
|
_ fs.Mover = (*Fs)(nil)
|
|
|
|
_ fs.DirMover = (*Fs)(nil)
|
2016-07-25 18:18:56 +00:00
|
|
|
// _ fs.PutUncheckeder = (*Fs)(nil)
|
|
|
|
_ fs.UnWrapper = (*Fs)(nil)
|
|
|
|
_ fs.ObjectInfo = (*ObjectInfo)(nil)
|
|
|
|
_ fs.Object = (*Object)(nil)
|
|
|
|
_ fs.ListOpts = (*ListOpts)(nil)
|
|
|
|
)
|