2016-07-25 20:18:56 +02:00
// Package crypt provides wrappers for Fs and Object which implement encryption
package crypt
import (
"fmt"
"io"
"path"
2016-12-19 15:09:59 +01:00
"strings"
2016-07-25 20:18:56 +02:00
"github.com/ncw/rclone/fs"
"github.com/pkg/errors"
)
2017-01-29 11:12:52 +01:00
// Globals
var (
// Flags
cryptShowMapping = fs . BoolP ( "crypt-show-mapping" , "" , false , "For all files listed show how the names encrypt." )
)
2016-07-25 20:18:56 +02:00
// Register with Fs
func init ( ) {
fs . Register ( & fs . RegInfo {
Name : "crypt" ,
Description : "Encrypt/Decrypt a remote" ,
NewFs : NewFs ,
Options : [ ] fs . Option { {
Name : "remote" ,
2016-10-08 11:34:59 +02:00
Help : "Remote to encrypt/decrypt.\nNormally should contain a ':' and a path, eg \"myremote:path/to/dir\",\n\"myremote:bucket\" or maybe \"myremote:\" (not recommended)." ,
2016-07-25 20:18:56 +02:00
} , {
2016-08-20 19:46:10 +02:00
Name : "filename_encryption" ,
Help : "How to encrypt the filenames." ,
2016-07-25 20:18:56 +02:00
Examples : [ ] fs . OptionExample {
{
2016-08-20 19:46:10 +02:00
Value : "off" ,
Help : "Don't encrypt the file names. Adds a \".bin\" extension only." ,
2016-07-25 20:18:56 +02:00
} , {
2016-08-20 19:46:10 +02:00
Value : "standard" ,
Help : "Encrypt the filenames see the docs for the details." ,
2017-03-12 19:14:36 +01:00
} , {
Value : "obfuscate" ,
Help : "Very simple filename obfuscation." ,
2016-07-25 20:18:56 +02:00
} ,
} ,
} , {
Name : "password" ,
Help : "Password or pass phrase for encryption." ,
IsPassword : true ,
2016-08-19 21:02:02 +02: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 20:18:56 +02:00
} } ,
} )
}
// NewFs contstructs an Fs from the path, container:path
func NewFs ( name , rpath string ) ( fs . Fs , error ) {
2016-12-20 19:03:09 +01:00
mode , err := NewNameEncryptionMode ( fs . ConfigFileGet ( name , "filename_encryption" , "standard" ) )
2016-08-20 19:46:10 +02:00
if err != nil {
return nil , err
}
2016-12-20 19:03:09 +01:00
password := fs . ConfigFileGet ( name , "password" , "" )
2016-07-25 20:18:56 +02:00
if password == "" {
return nil , errors . New ( "password not set in config file" )
}
2016-08-20 19:46:10 +02:00
password , err = fs . Reveal ( password )
2016-07-25 20:18:56 +02:00
if err != nil {
return nil , errors . Wrap ( err , "failed to decrypt password" )
}
2016-12-20 19:03:09 +01:00
salt := fs . ConfigFileGet ( name , "password2" , "" )
2016-08-19 21:02:02 +02:00
if salt != "" {
salt , err = fs . Reveal ( salt )
if err != nil {
return nil , errors . Wrap ( err , "failed to decrypt password2" )
}
}
2016-08-20 19:46:10 +02:00
cipher , err := newCipher ( mode , password , salt )
2016-07-25 20:18:56 +02:00
if err != nil {
return nil , errors . Wrap ( err , "failed to make cipher" )
}
2016-12-20 19:03:09 +01:00
remote := fs . ConfigFileGet ( name , "remote" )
2016-12-19 15:09:59 +01:00
if strings . HasPrefix ( remote , name + ":" ) {
return nil , errors . New ( "can't point crypt remote at itself - check the value of the remote setting" )
}
2016-08-20 19:46:10 +02:00
// Look for a file first
remotePath := path . Join ( remote , cipher . EncryptFileName ( rpath ) )
2016-07-25 20:18:56 +02:00
wrappedFs , err := fs . NewFs ( remotePath )
2016-08-20 19:46:10 +02: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 20:18:56 +02: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 19:46:10 +02:00
Fs : wrappedFs ,
2016-09-09 09:38:18 +02:00
name : name ,
root : rpath ,
2017-01-13 18:21:47 +01:00
cipher : cipher ,
mode : mode ,
2016-07-25 20:18:56 +02:00
}
2017-01-13 18:21:47 +01:00
// the features here are ones we could support, and they are
// ANDed with the ones from wrappedFs
f . features = ( & fs . Features {
2017-08-09 16:27:43 +02:00
CaseInsensitive : mode == NameEncryptionOff ,
DuplicateFiles : true ,
ReadMimeType : false , // MimeTypes not supported with crypt
WriteMimeType : false ,
BucketBased : true ,
CanHaveEmptyDirectories : true ,
2017-01-13 18:21:47 +01:00
} ) . Fill ( f ) . Mask ( wrappedFs )
2016-07-25 20:18:56 +02:00
return f , err
}
// Fs represents a wrapped fs.Fs
type Fs struct {
fs . Fs
2017-01-13 18:21:47 +01:00
name string
root string
features * fs . Features // optional features
cipher Cipher
mode NameEncryptionMode
2016-09-09 09:38:18 +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
2016-07-25 20:18:56 +02:00
}
2017-01-13 18:21:47 +01:00
// Features returns the optional features of this Fs
func ( f * Fs ) Features ( ) * fs . Features {
return f . features
}
2016-07-25 20:18:56 +02:00
// String returns a description of the FS
func ( f * Fs ) String ( ) string {
2017-04-10 18:52:31 +02:00
return fmt . Sprintf ( "Encrypted drive '%s:%s'" , f . name , f . root )
2016-07-25 20:18:56 +02:00
}
2017-06-11 23:43:31 +02:00
// Encrypt an object file name to entries.
func ( f * Fs ) add ( entries * fs . DirEntries , obj fs . Object ) {
remote := obj . Remote ( )
decryptedRemote , err := f . cipher . DecryptFileName ( remote )
if err != nil {
fs . Debugf ( remote , "Skipping undecryptable file name: %v" , err )
return
}
if * cryptShowMapping {
fs . Logf ( decryptedRemote , "Encrypts to %q" , remote )
}
* entries = append ( * entries , f . newObject ( obj ) )
}
// Encrypt an directory file name to entries.
2017-06-30 14:37:29 +02:00
func ( f * Fs ) addDir ( entries * fs . DirEntries , dir fs . Directory ) {
remote := dir . Remote ( )
2017-06-11 23:43:31 +02:00
decryptedRemote , err := f . cipher . DecryptDirName ( remote )
if err != nil {
fs . Debugf ( remote , "Skipping undecryptable dir name: %v" , err )
return
}
if * cryptShowMapping {
fs . Logf ( decryptedRemote , "Encrypts to %q" , remote )
}
* entries = append ( * entries , f . newDir ( dir ) )
}
2017-06-15 17:45:21 +02:00
// Encrypt some directory entries. This alters entries returning it as newEntries.
2017-06-11 23:43:31 +02:00
func ( f * Fs ) encryptEntries ( entries fs . DirEntries ) ( newEntries fs . DirEntries , err error ) {
2017-06-15 17:45:21 +02:00
newEntries = entries [ : 0 ] // in place filter
2017-06-11 23:43:31 +02:00
for _ , entry := range entries {
switch x := entry . ( type ) {
case fs . Object :
f . add ( & newEntries , x )
2017-06-30 14:37:29 +02:00
case fs . Directory :
2017-06-11 23:43:31 +02:00
f . addDir ( & newEntries , x )
default :
return nil , errors . Errorf ( "Unknown object type %T" , entry )
}
}
return newEntries , 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 ) {
entries , err = f . Fs . List ( f . cipher . EncryptDirName ( dir ) )
if err != nil {
return nil , err
}
return f . encryptEntries ( entries )
}
// 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 ) {
return f . Fs . Features ( ) . ListR ( f . cipher . EncryptDirName ( dir ) , func ( entries fs . DirEntries ) error {
newEntries , err := f . encryptEntries ( entries )
if err != nil {
return err
}
return callback ( newEntries )
} )
2016-07-25 20:18:56 +02:00
}
// NewObject finds the Object at remote.
func ( f * Fs ) NewObject ( remote string ) ( fs . Object , error ) {
2016-08-20 19:46:10 +02:00
o , err := f . Fs . NewObject ( f . cipher . EncryptFileName ( remote ) )
2016-07-25 20:18:56 +02:00
if err != nil {
return nil , err
}
return f . newObject ( o ) , nil
}
2017-09-27 17:46:28 +02:00
type putFn func ( in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error )
// put implements Put or PutStream
func ( f * Fs ) put ( in io . Reader , src fs . ObjectInfo , options [ ] fs . OpenOption , put putFn ) ( fs . Object , error ) {
2016-07-25 20:18:56 +02:00
wrappedIn , err := f . cipher . EncryptData ( in )
if err != nil {
return nil , err
}
2017-09-27 17:46:28 +02:00
o , err := put ( wrappedIn , f . newObjectInfo ( src ) , options ... )
2016-07-25 20:18:56 +02:00
if err != nil {
return nil , err
}
return f . newObject ( o ) , nil
}
2017-09-27 17:46:28 +02:00
// 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 , options ... fs . OpenOption ) ( fs . Object , error ) {
return f . put ( in , src , options , f . Fs . Put )
}
2017-08-03 21:42:35 +02:00
// PutStream uploads to the remote path with the modTime given of indeterminate size
func ( f * Fs ) PutStream ( in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
2017-09-27 17:46:28 +02:00
return f . put ( in , src , options , f . Fs . Features ( ) . PutStream )
2017-08-03 21:42:35 +02:00
}
2016-07-25 20:18:56 +02:00
// Hashes returns the supported hash sets.
func ( f * Fs ) Hashes ( ) fs . HashSet {
return fs . HashSet ( fs . HashNone )
}
2016-12-06 16:13:29 +01:00
// Mkdir makes the directory (container, bucket)
//
// Shouldn't return an error if it already exists
func ( f * Fs ) Mkdir ( dir string ) error {
return f . Fs . Mkdir ( f . cipher . EncryptDirName ( dir ) )
}
// Rmdir removes the directory (container, bucket) if empty
//
// Return an error if it doesn't exist or isn't empty
func ( f * Fs ) Rmdir ( dir string ) error {
return f . Fs . Rmdir ( f . cipher . EncryptDirName ( dir ) )
}
2016-07-25 20:18:56 +02:00
// 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 {
2017-01-13 18:21:47 +01:00
do := f . Fs . Features ( ) . Purge
if do == nil {
2016-07-25 20:18:56 +02:00
return fs . ErrorCantPurge
}
2017-01-13 18:21:47 +01:00
return do ( )
2016-07-25 20:18:56 +02:00
}
// 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 ) {
2017-01-13 18:21:47 +01:00
do := f . Fs . Features ( ) . Copy
if do == nil {
2016-07-25 20:18:56 +02:00
return nil , fs . ErrorCantCopy
}
o , ok := src . ( * Object )
if ! ok {
return nil , fs . ErrorCantCopy
}
2017-01-13 18:21:47 +01:00
oResult , err := do ( o . Object , f . cipher . EncryptFileName ( remote ) )
2016-07-25 20:18:56 +02: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 ) {
2017-01-13 18:21:47 +01:00
do := f . Fs . Features ( ) . Move
if do == nil {
2016-07-25 20:18:56 +02:00
return nil , fs . ErrorCantMove
}
o , ok := src . ( * Object )
if ! ok {
2016-08-23 18:43:43 +02:00
return nil , fs . ErrorCantMove
2016-07-25 20:18:56 +02:00
}
2017-01-13 18:21:47 +01:00
oResult , err := do ( o . Object , f . cipher . EncryptFileName ( remote ) )
2016-07-25 20:18:56 +02:00
if err != nil {
return nil , err
}
return f . newObject ( oResult ) , nil
}
2017-02-05 22:20:56 +01:00
// DirMove moves src, srcRemote to this remote at dstRemote
// using server side move operations.
2016-08-23 18:43:43 +02:00
//
// 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
2017-02-05 22:20:56 +01:00
func ( f * Fs ) DirMove ( src fs . Fs , srcRemote , dstRemote string ) error {
2017-01-13 18:21:47 +01:00
do := f . Fs . Features ( ) . DirMove
if do == nil {
2016-08-23 18:43:43 +02:00
return fs . ErrorCantDirMove
}
srcFs , ok := src . ( * Fs )
if ! ok {
2017-02-09 12:01:20 +01:00
fs . Debugf ( srcFs , "Can't move directory - not same remote type" )
2016-08-23 18:43:43 +02:00
return fs . ErrorCantDirMove
}
2017-02-05 22:20:56 +01:00
return do ( srcFs . Fs , f . cipher . EncryptDirName ( srcRemote ) , f . cipher . EncryptDirName ( dstRemote ) )
2017-01-13 18:21:47 +01:00
}
// 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.
2017-05-28 13:44:22 +02:00
func ( f * Fs ) PutUnchecked ( in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
2017-01-13 18:21:47 +01:00
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 ( )
2016-08-23 18:43:43 +02:00
}
2016-07-25 20:18:56 +02:00
// UnWrap returns the Fs that this Fs is wrapping
func ( f * Fs ) UnWrap ( ) fs . Fs {
return f . Fs
}
2017-09-20 13:40:07 +02:00
// DecryptFileName returns a decrypted file name
func ( f * Fs ) DecryptFileName ( encryptedFileName string ) ( string , error ) {
return f . cipher . DecryptFileName ( encryptedFileName )
}
2017-02-12 17:30:18 +01:00
// ComputeHash takes the nonce from o, and encrypts the contents of
// src with it, and calcuates the hash given by HashType on the fly
//
// Note that we break lots of encapsulation in this function.
func ( f * Fs ) ComputeHash ( o * Object , src fs . Object , hashType fs . HashType ) ( hash string , err error ) {
// Read the nonce - opening the file is sufficient to read the nonce in
in , err := o . Open ( )
if err != nil {
return "" , errors . Wrap ( err , "failed to read nonce" )
}
nonce := in . ( * decrypter ) . nonce
// fs.Debugf(o, "Read nonce % 2x", nonce)
// Check nonce isn't all zeros
isZero := true
for i := range nonce {
if nonce [ i ] != 0 {
isZero = false
}
}
if isZero {
fs . Errorf ( o , "empty nonce read" )
}
// Close in once we have read the nonce
err = in . Close ( )
if err != nil {
return "" , errors . Wrap ( err , "failed to close nonce read" )
}
// Open the src for input
in , err = src . Open ( )
if err != nil {
return "" , errors . Wrap ( err , "failed to open src" )
}
defer fs . CheckClose ( in , & err )
// Now encrypt the src with the nonce
out , err := f . cipher . ( * cipher ) . newEncrypter ( in , & nonce )
if err != nil {
return "" , errors . Wrap ( err , "failed to make encrypter" )
}
// pipe into hash
m := fs . NewMultiHasher ( )
_ , err = io . Copy ( m , out )
if err != nil {
return "" , errors . Wrap ( err , "failed to hash data" )
}
return m . Sums ( ) [ hashType ] , nil
}
2016-07-25 20:18:56 +02:00
// 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 19:46:10 +02:00
decryptedName , err := o . f . cipher . DecryptFileName ( remote )
2016-07-25 20:18:56 +02:00
if err != nil {
2017-02-09 12:01:20 +01:00
fs . Debugf ( remote , "Undecryptable file name: %v" , err )
2016-07-25 20:18:56 +02:00
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 {
2017-02-09 12:01:20 +01:00
fs . Debugf ( o , "Bad size for decrypt: %v" , err )
2016-07-25 20:18:56 +02:00
}
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
}
2017-02-12 17:30:18 +01:00
// UnWrap returns the wrapped Object
func ( o * Object ) UnWrap ( ) fs . Object {
return o . Object
}
2016-07-25 20:18:56 +02:00
// Open opens the file for read. Call Close() on the returned io.ReadCloser
2016-10-20 18:47:33 +02:00
func ( o * Object ) Open ( options ... fs . OpenOption ) ( rc io . ReadCloser , err error ) {
2016-09-10 12:29:57 +02:00
var offset int64
for _ , option := range options {
switch x := option . ( type ) {
case * fs . SeekOption :
offset = x . Offset
default :
if option . Mandatory ( ) {
2017-02-09 12:01:20 +01:00
fs . Logf ( o , "Unsupported mandatory option: %v" , option )
2016-09-10 12:29:57 +02:00
}
}
}
2016-10-20 18:47:33 +02:00
rc , err = o . f . cipher . DecryptDataSeek ( func ( underlyingOffset int64 ) ( io . ReadCloser , error ) {
if underlyingOffset == 0 {
// Open with no seek
return o . Object . Open ( )
}
// Open stream with a seek of underlyingOffset
return o . Object . Open ( & fs . SeekOption { Offset : underlyingOffset } )
} , offset )
2016-09-10 12:29:57 +02:00
if err != nil {
return nil , err
}
return rc , err
2016-07-25 20:18:56 +02:00
}
// Update in to the object with the modTime given of the given size
2017-05-28 13:44:22 +02:00
func ( o * Object ) Update ( in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) error {
2016-07-25 20:18:56 +02:00
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
2017-06-30 14:37:29 +02:00
func ( f * Fs ) newDir ( dir fs . Directory ) fs . Directory {
new := fs . NewDirCopy ( dir )
remote := dir . Remote ( )
2016-08-20 19:46:10 +02:00
decryptedRemote , err := f . cipher . DecryptDirName ( remote )
2016-07-25 20:18:56 +02:00
if err != nil {
2017-02-09 12:01:20 +01:00
fs . Debugf ( remote , "Undecryptable dir name: %v" , err )
2016-07-25 20:18:56 +02:00
} else {
2017-06-30 14:37:29 +02:00
new . SetRemote ( decryptedRemote )
2016-07-25 20:18:56 +02:00
}
2017-06-30 14:37:29 +02:00
return new
2016-07-25 20:18:56 +02:00
}
// 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 19:46:10 +02:00
return o . f . cipher . EncryptFileName ( o . ObjectInfo . Remote ( ) )
2016-07-25 20:18:56 +02:00
}
// Size returns the size of the file
func ( o * ObjectInfo ) Size ( ) int64 {
2017-09-27 17:46:28 +02:00
size := o . ObjectInfo . Size ( )
if size < 0 {
return size
}
return o . f . cipher . EncryptedSize ( size )
2016-07-25 20:18:56 +02:00
}
2016-08-25 22:26:55 +02: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 20:18:56 +02:00
// Check the interfaces are satisfied
var (
2017-01-13 18:21:47 +01:00
_ fs . Fs = ( * Fs ) ( nil )
_ fs . Purger = ( * Fs ) ( nil )
_ fs . Copier = ( * Fs ) ( nil )
_ fs . Mover = ( * Fs ) ( nil )
_ fs . DirMover = ( * Fs ) ( nil )
_ fs . PutUncheckeder = ( * Fs ) ( nil )
2017-08-03 21:42:35 +02:00
_ fs . PutStreamer = ( * Fs ) ( nil )
2017-01-13 18:21:47 +01:00
_ fs . CleanUpper = ( * Fs ) ( nil )
_ fs . UnWrapper = ( * Fs ) ( nil )
2017-06-11 23:43:31 +02:00
_ fs . ListRer = ( * Fs ) ( nil )
2017-01-13 18:21:47 +01:00
_ fs . ObjectInfo = ( * ObjectInfo ) ( nil )
_ fs . Object = ( * Object ) ( nil )
2016-07-25 20:18:56 +02:00
)