mirror of
https://github.com/rclone/rclone
synced 2024-11-27 05:23:40 +01:00
296ceadda6
This also factors the config questions into a state based mechanism so a backend can be configured using the same dialog as rclone config but remotely.
1605 lines
48 KiB
Go
1605 lines
48 KiB
Go
// Package fs is a generic file system interface for rclone object storage systems
|
|
package fs
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/rclone/rclone/fs/config/configmap"
|
|
"github.com/rclone/rclone/fs/config/configstruct"
|
|
"github.com/rclone/rclone/fs/fserrors"
|
|
"github.com/rclone/rclone/fs/fspath"
|
|
"github.com/rclone/rclone/fs/hash"
|
|
"github.com/rclone/rclone/lib/pacer"
|
|
)
|
|
|
|
// EntryType can be associated with remote paths to identify their type
|
|
type EntryType int
|
|
|
|
// Constants
|
|
const (
|
|
// ModTimeNotSupported is a very large precision value to show
|
|
// mod time isn't supported on this Fs
|
|
ModTimeNotSupported = 100 * 365 * 24 * time.Hour
|
|
// MaxLevel is a sentinel representing an infinite depth for listings
|
|
MaxLevel = math.MaxInt32
|
|
// EntryDirectory should be used to classify remote paths in directories
|
|
EntryDirectory EntryType = iota // 0
|
|
// EntryObject should be used to classify remote paths in objects
|
|
EntryObject // 1
|
|
)
|
|
|
|
// Globals
|
|
var (
|
|
// Filesystem registry
|
|
Registry []*RegInfo
|
|
// ErrorNotFoundInConfigFile is returned by NewFs if not found in config file
|
|
ErrorNotFoundInConfigFile = errors.New("didn't find section in config file")
|
|
ErrorCantPurge = errors.New("can't purge directory")
|
|
ErrorCantCopy = errors.New("can't copy object - incompatible remotes")
|
|
ErrorCantMove = errors.New("can't move object - incompatible remotes")
|
|
ErrorCantDirMove = errors.New("can't move directory - incompatible remotes")
|
|
ErrorCantUploadEmptyFiles = errors.New("can't upload empty files to this remote")
|
|
ErrorDirExists = errors.New("can't copy directory - destination already exists")
|
|
ErrorCantSetModTime = errors.New("can't set modified time")
|
|
ErrorCantSetModTimeWithoutDelete = errors.New("can't set modified time without deleting existing object")
|
|
ErrorDirNotFound = errors.New("directory not found")
|
|
ErrorObjectNotFound = errors.New("object not found")
|
|
ErrorLevelNotSupported = errors.New("level value not supported")
|
|
ErrorListAborted = errors.New("list aborted")
|
|
ErrorListBucketRequired = errors.New("bucket or container name is needed in remote")
|
|
ErrorIsFile = errors.New("is a file not a directory")
|
|
ErrorNotAFile = errors.New("is not a regular file")
|
|
ErrorNotDeleting = errors.New("not deleting files as there were IO errors")
|
|
ErrorNotDeletingDirs = errors.New("not deleting directories as there were IO errors")
|
|
ErrorOverlapping = errors.New("can't sync or move files on overlapping remotes")
|
|
ErrorDirectoryNotEmpty = errors.New("directory not empty")
|
|
ErrorImmutableModified = errors.New("immutable file modified")
|
|
ErrorPermissionDenied = errors.New("permission denied")
|
|
ErrorCantShareDirectories = errors.New("this backend can't share directories with link")
|
|
ErrorNotImplemented = errors.New("optional feature not implemented")
|
|
ErrorCommandNotFound = errors.New("command not found")
|
|
ErrorFileNameTooLong = errors.New("file name too long")
|
|
)
|
|
|
|
// RegInfo provides information about a filesystem
|
|
type RegInfo struct {
|
|
// Name of this fs
|
|
Name string
|
|
// Description of this fs - defaults to Name
|
|
Description string
|
|
// Prefix for command line flags for this fs - defaults to Name if not set
|
|
Prefix string
|
|
// Create a new file system. If root refers to an existing
|
|
// object, then it should return an Fs which which points to
|
|
// the parent of that object and ErrorIsFile.
|
|
NewFs func(ctx context.Context, name string, root string, config configmap.Mapper) (Fs, error) `json:"-"`
|
|
// Function to call to help with config - see docs for ConfigIn for more info
|
|
Config func(ctx context.Context, name string, m configmap.Mapper, configIn ConfigIn) (*ConfigOut, error) `json:"-"`
|
|
// Options for the Fs configuration
|
|
Options Options
|
|
// The command help, if any
|
|
CommandHelp []CommandHelp
|
|
}
|
|
|
|
// FileName returns the on disk file name for this backend
|
|
func (ri *RegInfo) FileName() string {
|
|
return strings.Replace(ri.Name, " ", "", -1)
|
|
}
|
|
|
|
// Options is a slice of configuration Option for a backend
|
|
type Options []Option
|
|
|
|
// Set the default values for the options
|
|
func (os Options) setValues() {
|
|
for i := range os {
|
|
o := &os[i]
|
|
if o.Default == nil {
|
|
o.Default = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the Option corresponding to name or return nil if not found
|
|
func (os Options) Get(name string) *Option {
|
|
for i := range os {
|
|
opt := &os[i]
|
|
if opt.Name == name {
|
|
return opt
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Overridden discovers which config items have been overridden in the
|
|
// configmap passed in, either by the config string, command line
|
|
// flags or environment variables
|
|
func (os Options) Overridden(m *configmap.Map) configmap.Simple {
|
|
var overridden = configmap.Simple{}
|
|
for i := range os {
|
|
opt := &os[i]
|
|
value, isSet := m.GetPriority(opt.Name, configmap.PriorityNormal)
|
|
if isSet {
|
|
overridden.Set(opt.Name, value)
|
|
}
|
|
}
|
|
return overridden
|
|
}
|
|
|
|
// NonDefault discovers which config values aren't at their default
|
|
func (os Options) NonDefault(m configmap.Getter) configmap.Simple {
|
|
var nonDefault = configmap.Simple{}
|
|
for i := range os {
|
|
opt := &os[i]
|
|
value, isSet := m.Get(opt.Name)
|
|
if !isSet {
|
|
continue
|
|
}
|
|
defaultValue := fmt.Sprint(opt.Default)
|
|
if value != defaultValue {
|
|
nonDefault.Set(opt.Name, value)
|
|
}
|
|
}
|
|
return nonDefault
|
|
}
|
|
|
|
// HasAdvanced discovers if any options have an Advanced setting
|
|
func (os Options) HasAdvanced() bool {
|
|
for i := range os {
|
|
opt := &os[i]
|
|
if opt.Advanced {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// OptionVisibility controls whether the options are visible in the
|
|
// configurator or the command line.
|
|
type OptionVisibility byte
|
|
|
|
// Constants Option.Hide
|
|
const (
|
|
OptionHideCommandLine OptionVisibility = 1 << iota
|
|
OptionHideConfigurator
|
|
OptionHideBoth = OptionHideCommandLine | OptionHideConfigurator
|
|
)
|
|
|
|
// Option is describes an option for the config wizard
|
|
//
|
|
// This also describes command line options and environment variables
|
|
type Option struct {
|
|
Name string // name of the option in snake_case
|
|
Help string // Help, the first line only is used for the command line help
|
|
Provider string // Set to filter on provider
|
|
Default interface{} // default value, nil => ""
|
|
Value interface{} // value to be set by flags
|
|
Examples OptionExamples `json:",omitempty"` // config examples
|
|
ShortOpt string // the short option for this if required
|
|
Hide OptionVisibility // set this to hide the config from the configurator or the command line
|
|
Required bool // this option is required
|
|
IsPassword bool // set if the option is a password
|
|
NoPrefix bool // set if the option for this should not use the backend prefix
|
|
Advanced bool // set if this is an advanced config option
|
|
Exclusive bool // set if the answer can only be one of the examples
|
|
}
|
|
|
|
// BaseOption is an alias for Option used internally
|
|
type BaseOption Option
|
|
|
|
// MarshalJSON turns an Option into JSON
|
|
//
|
|
// It adds some generated fields for ease of use
|
|
// - DefaultStr - a string rendering of Default
|
|
// - ValueStr - a string rendering of Value
|
|
// - Type - the type of the option
|
|
func (o *Option) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(struct {
|
|
BaseOption
|
|
DefaultStr string
|
|
ValueStr string
|
|
Type string
|
|
}{
|
|
BaseOption: BaseOption(*o),
|
|
DefaultStr: fmt.Sprint(o.Default),
|
|
ValueStr: o.String(),
|
|
Type: o.Type(),
|
|
})
|
|
}
|
|
|
|
// GetValue gets the current current value which is the default if not set
|
|
func (o *Option) GetValue() interface{} {
|
|
val := o.Value
|
|
if val == nil {
|
|
val = o.Default
|
|
if val == nil {
|
|
val = ""
|
|
}
|
|
}
|
|
return val
|
|
}
|
|
|
|
// String turns Option into a string
|
|
func (o *Option) String() string {
|
|
return fmt.Sprint(o.GetValue())
|
|
}
|
|
|
|
// Set an Option from a string
|
|
func (o *Option) Set(s string) (err error) {
|
|
newValue, err := configstruct.StringToInterface(o.GetValue(), s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.Value = newValue
|
|
return nil
|
|
}
|
|
|
|
// Type of the value
|
|
func (o *Option) Type() string {
|
|
return reflect.TypeOf(o.GetValue()).Name()
|
|
}
|
|
|
|
// FlagName for the option
|
|
func (o *Option) FlagName(prefix string) string {
|
|
name := strings.Replace(o.Name, "_", "-", -1) // convert snake_case to kebab-case
|
|
if !o.NoPrefix {
|
|
name = prefix + "-" + name
|
|
}
|
|
return name
|
|
}
|
|
|
|
// EnvVarName for the option
|
|
func (o *Option) EnvVarName(prefix string) string {
|
|
return OptionToEnv(prefix + "-" + o.Name)
|
|
}
|
|
|
|
// Copy makes a shallow copy of the option
|
|
func (o *Option) Copy() *Option {
|
|
copy := new(Option)
|
|
*copy = *o
|
|
return copy
|
|
}
|
|
|
|
// OptionExamples is a slice of examples
|
|
type OptionExamples []OptionExample
|
|
|
|
// Len is part of sort.Interface.
|
|
func (os OptionExamples) Len() int { return len(os) }
|
|
|
|
// Swap is part of sort.Interface.
|
|
func (os OptionExamples) Swap(i, j int) { os[i], os[j] = os[j], os[i] }
|
|
|
|
// Less is part of sort.Interface.
|
|
func (os OptionExamples) Less(i, j int) bool { return os[i].Help < os[j].Help }
|
|
|
|
// Sort sorts an OptionExamples
|
|
func (os OptionExamples) Sort() { sort.Sort(os) }
|
|
|
|
// OptionExample describes an example for an Option
|
|
type OptionExample struct {
|
|
Value string
|
|
Help string
|
|
Provider string
|
|
}
|
|
|
|
// Register a filesystem
|
|
//
|
|
// Fs modules should use this in an init() function
|
|
func Register(info *RegInfo) {
|
|
info.Options.setValues()
|
|
if info.Prefix == "" {
|
|
info.Prefix = info.Name
|
|
}
|
|
Registry = append(Registry, info)
|
|
}
|
|
|
|
// Fs is the interface a cloud storage system must provide
|
|
type Fs interface {
|
|
Info
|
|
|
|
// 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.
|
|
List(ctx context.Context, dir string) (entries DirEntries, err error)
|
|
|
|
// NewObject finds the Object at remote. If it can't be found
|
|
// it returns the error ErrorObjectNotFound.
|
|
NewObject(ctx context.Context, remote string) (Object, error)
|
|
|
|
// Put in to the remote path with the modTime given of the given size
|
|
//
|
|
// When called from outside an Fs by rclone, src.Size() will always be >= 0.
|
|
// But for unknown-sized objects (indicated by src.Size() == -1), Put should either
|
|
// return an error or upload it properly (rather than e.g. calling panic).
|
|
//
|
|
// 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
|
|
Put(ctx context.Context, in io.Reader, src ObjectInfo, options ...OpenOption) (Object, error)
|
|
|
|
// Mkdir makes the directory (container, bucket)
|
|
//
|
|
// Shouldn't return an error if it already exists
|
|
Mkdir(ctx context.Context, dir string) error
|
|
|
|
// Rmdir removes the directory (container, bucket) if empty
|
|
//
|
|
// Return an error if it doesn't exist or isn't empty
|
|
Rmdir(ctx context.Context, dir string) error
|
|
}
|
|
|
|
// Info provides a read only interface to information about a filesystem.
|
|
type Info interface {
|
|
// Name of the remote (as passed into NewFs)
|
|
Name() string
|
|
|
|
// Root of the remote (as passed into NewFs)
|
|
Root() string
|
|
|
|
// String returns a description of the FS
|
|
String() string
|
|
|
|
// Precision of the ModTimes in this Fs
|
|
Precision() time.Duration
|
|
|
|
// Returns the supported hash types of the filesystem
|
|
Hashes() hash.Set
|
|
|
|
// Features returns the optional features of this Fs
|
|
Features() *Features
|
|
}
|
|
|
|
// Object is a filesystem like object provided by an Fs
|
|
type Object interface {
|
|
ObjectInfo
|
|
|
|
// SetModTime sets the metadata on the object to set the modification date
|
|
SetModTime(ctx context.Context, t time.Time) error
|
|
|
|
// Open opens the file for read. Call Close() on the returned io.ReadCloser
|
|
Open(ctx context.Context, options ...OpenOption) (io.ReadCloser, error)
|
|
|
|
// Update in to the object with the modTime given of the given size
|
|
//
|
|
// When called from outside an Fs by rclone, src.Size() will always be >= 0.
|
|
// But for unknown-sized objects (indicated by src.Size() == -1), Upload should either
|
|
// return an error or update the object properly (rather than e.g. calling panic).
|
|
Update(ctx context.Context, in io.Reader, src ObjectInfo, options ...OpenOption) error
|
|
|
|
// Removes this object
|
|
Remove(ctx context.Context) error
|
|
}
|
|
|
|
// ObjectInfo provides read only information about an object.
|
|
type ObjectInfo interface {
|
|
DirEntry
|
|
|
|
// Fs returns read only access to the Fs that this object is part of
|
|
Fs() Info
|
|
|
|
// Hash returns the selected checksum of the file
|
|
// If no checksum is available it returns ""
|
|
Hash(ctx context.Context, ty hash.Type) (string, error)
|
|
|
|
// Storable says whether this object can be stored
|
|
Storable() bool
|
|
}
|
|
|
|
// DirEntry provides read only information about the common subset of
|
|
// a Dir or Object. These are returned from directory listings - type
|
|
// assert them into the correct type.
|
|
type DirEntry interface {
|
|
// String returns a description of the Object
|
|
String() string
|
|
|
|
// Remote returns the remote path
|
|
Remote() string
|
|
|
|
// ModTime returns the modification date of the file
|
|
// It should return a best guess if one isn't available
|
|
ModTime(context.Context) time.Time
|
|
|
|
// Size returns the size of the file
|
|
Size() int64
|
|
}
|
|
|
|
// Directory is a filesystem like directory provided by an Fs
|
|
type Directory interface {
|
|
DirEntry
|
|
|
|
// Items returns the count of items in this directory or this
|
|
// directory and subdirectories if known, -1 for unknown
|
|
Items() int64
|
|
|
|
// ID returns the internal ID of this directory if known, or
|
|
// "" otherwise
|
|
ID() string
|
|
}
|
|
|
|
// MimeTyper is an optional interface for Object
|
|
type MimeTyper interface {
|
|
// MimeType returns the content type of the Object if
|
|
// known, or "" if not
|
|
MimeType(ctx context.Context) string
|
|
}
|
|
|
|
// IDer is an optional interface for Object
|
|
type IDer interface {
|
|
// ID returns the ID of the Object if known, or "" if not
|
|
ID() string
|
|
}
|
|
|
|
// ParentIDer is an optional interface for Object
|
|
type ParentIDer interface {
|
|
// ParentID returns the ID of the parent directory if known or nil if not
|
|
ParentID() string
|
|
}
|
|
|
|
// ObjectUnWrapper is an optional interface for Object
|
|
type ObjectUnWrapper interface {
|
|
// UnWrap returns the Object that this Object is wrapping or
|
|
// nil if it isn't wrapping anything
|
|
UnWrap() Object
|
|
}
|
|
|
|
// SetTierer is an optional interface for Object
|
|
type SetTierer interface {
|
|
// SetTier performs changing storage tier of the Object if
|
|
// multiple storage classes supported
|
|
SetTier(tier string) error
|
|
}
|
|
|
|
// GetTierer is an optional interface for Object
|
|
type GetTierer interface {
|
|
// GetTier returns storage tier or class of the Object
|
|
GetTier() string
|
|
}
|
|
|
|
// FullObjectInfo contains all the read-only optional interfaces
|
|
//
|
|
// Use for checking making wrapping ObjectInfos implement everything
|
|
type FullObjectInfo interface {
|
|
ObjectInfo
|
|
MimeTyper
|
|
IDer
|
|
ObjectUnWrapper
|
|
GetTierer
|
|
}
|
|
|
|
// FullObject contains all the optional interfaces for Object
|
|
//
|
|
// Use for checking making wrapping Objects implement everything
|
|
type FullObject interface {
|
|
Object
|
|
MimeTyper
|
|
IDer
|
|
ObjectUnWrapper
|
|
GetTierer
|
|
SetTierer
|
|
}
|
|
|
|
// ObjectOptionalInterfaces returns the names of supported and
|
|
// unsupported optional interfaces for an Object
|
|
func ObjectOptionalInterfaces(o Object) (supported, unsupported []string) {
|
|
store := func(ok bool, name string) {
|
|
if ok {
|
|
supported = append(supported, name)
|
|
} else {
|
|
unsupported = append(unsupported, name)
|
|
}
|
|
}
|
|
|
|
_, ok := o.(MimeTyper)
|
|
store(ok, "MimeType")
|
|
|
|
_, ok = o.(IDer)
|
|
store(ok, "ID")
|
|
|
|
_, ok = o.(ObjectUnWrapper)
|
|
store(ok, "UnWrap")
|
|
|
|
_, ok = o.(SetTierer)
|
|
store(ok, "SetTier")
|
|
|
|
_, ok = o.(GetTierer)
|
|
store(ok, "GetTier")
|
|
|
|
return supported, unsupported
|
|
}
|
|
|
|
// ListRCallback defines a callback function for ListR to use
|
|
//
|
|
// It is called for each tranche of entries read from the listing and
|
|
// if it returns an error, the listing stops.
|
|
type ListRCallback func(entries DirEntries) error
|
|
|
|
// ListRFn is defines the call used to recursively list a directory
|
|
type ListRFn func(ctx context.Context, dir string, callback ListRCallback) error
|
|
|
|
// NewUsageValue makes a valid value
|
|
func NewUsageValue(value int64) *int64 {
|
|
p := new(int64)
|
|
*p = value
|
|
return p
|
|
}
|
|
|
|
// Usage is returned by the About call
|
|
//
|
|
// If a value is nil then it isn't supported by that backend
|
|
type Usage struct {
|
|
Total *int64 `json:"total,omitempty"` // quota of bytes that can be used
|
|
Used *int64 `json:"used,omitempty"` // bytes in use
|
|
Trashed *int64 `json:"trashed,omitempty"` // bytes in trash
|
|
Other *int64 `json:"other,omitempty"` // other usage e.g. gmail in drive
|
|
Free *int64 `json:"free,omitempty"` // bytes which can be uploaded before reaching the quota
|
|
Objects *int64 `json:"objects,omitempty"` // objects in the storage system
|
|
}
|
|
|
|
// WriterAtCloser wraps io.WriterAt and io.Closer
|
|
type WriterAtCloser interface {
|
|
io.WriterAt
|
|
io.Closer
|
|
}
|
|
|
|
// Features describe the optional features of the Fs
|
|
type Features struct {
|
|
// Feature flags, whether Fs
|
|
CaseInsensitive bool // has case insensitive files
|
|
DuplicateFiles bool // allows duplicate files
|
|
ReadMimeType bool // can read the mime type of objects
|
|
WriteMimeType bool // can set the mime type of objects
|
|
CanHaveEmptyDirectories bool // can have empty directories
|
|
BucketBased bool // is bucket based (like s3, swift, etc.)
|
|
BucketBasedRootOK bool // is bucket based and can use from root
|
|
SetTier bool // allows set tier functionality on objects
|
|
GetTier bool // allows to retrieve storage tier of objects
|
|
ServerSideAcrossConfigs bool // can server-side copy between different remotes of the same type
|
|
IsLocal bool // is the local backend
|
|
SlowModTime bool // if calling ModTime() generally takes an extra transaction
|
|
SlowHash bool // if calling Hash() generally takes an extra transaction
|
|
|
|
// Purge all files in the directory specified
|
|
//
|
|
// 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
|
|
Purge func(ctx context.Context, dir string) error
|
|
|
|
// 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
|
|
Copy func(ctx context.Context, src Object, remote string) (Object, error)
|
|
|
|
// 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
|
|
Move func(ctx context.Context, src Object, remote string) (Object, error)
|
|
|
|
// 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
|
|
DirMove func(ctx context.Context, src Fs, srcRemote, dstRemote string) error
|
|
|
|
// ChangeNotify calls the passed function with a path
|
|
// that has had changes. If the implementation
|
|
// uses polling, it should adhere to the given interval.
|
|
ChangeNotify func(context.Context, func(string, EntryType), <-chan time.Duration)
|
|
|
|
// UnWrap returns the Fs that this Fs is wrapping
|
|
UnWrap func() Fs
|
|
|
|
// WrapFs returns the Fs that is wrapping this Fs
|
|
WrapFs func() Fs
|
|
|
|
// SetWrapper sets the Fs that is wrapping this Fs
|
|
SetWrapper func(f Fs)
|
|
|
|
// DirCacheFlush resets the directory cache - used in testing
|
|
// as an optional interface
|
|
DirCacheFlush func()
|
|
|
|
// PublicLink generates a public link to the remote path (usually readable by anyone)
|
|
PublicLink func(ctx context.Context, remote string, expire Duration, unlink bool) (string, error)
|
|
|
|
// 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
|
|
//
|
|
// May create duplicates or return errors if src already
|
|
// exists.
|
|
PutUnchecked func(ctx context.Context, in io.Reader, src ObjectInfo, options ...OpenOption) (Object, error)
|
|
|
|
// PutStream uploads to the remote path with the modTime given of indeterminate 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
|
|
PutStream func(ctx context.Context, in io.Reader, src ObjectInfo, options ...OpenOption) (Object, error)
|
|
|
|
// MergeDirs merges the contents of all the directories passed
|
|
// in into the first one and rmdirs the other directories.
|
|
MergeDirs func(ctx context.Context, dirs []Directory) error
|
|
|
|
// 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.
|
|
CleanUp func(ctx context.Context) error
|
|
|
|
// 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.
|
|
ListR ListRFn
|
|
|
|
// About gets quota information from the Fs
|
|
About func(ctx context.Context) (*Usage, error)
|
|
|
|
// OpenWriterAt opens with a handle for random access writes
|
|
//
|
|
// Pass in the remote desired and the size if known.
|
|
//
|
|
// It truncates any existing object
|
|
OpenWriterAt func(ctx context.Context, remote string, size int64) (WriterAtCloser, error)
|
|
|
|
// UserInfo returns info about the connected user
|
|
UserInfo func(ctx context.Context) (map[string]string, error)
|
|
|
|
// Disconnect the current user
|
|
Disconnect func(ctx context.Context) error
|
|
|
|
// Command the backend to run a named command
|
|
//
|
|
// The command run is name
|
|
// args may be used to read arguments from
|
|
// opts may be used to read optional arguments from
|
|
//
|
|
// The result should be capable of being JSON encoded
|
|
// If it is a string or a []string it will be shown to the user
|
|
// otherwise it will be JSON encoded and shown to the user like that
|
|
Command func(ctx context.Context, name string, arg []string, opt map[string]string) (interface{}, error)
|
|
|
|
// Shutdown the backend, closing any background tasks and any
|
|
// cached connections.
|
|
Shutdown func(ctx context.Context) error
|
|
}
|
|
|
|
// Disable nil's out the named feature. If it isn't found then it
|
|
// will log a message.
|
|
func (ft *Features) Disable(name string) *Features {
|
|
v := reflect.ValueOf(ft).Elem()
|
|
vType := v.Type()
|
|
for i := 0; i < v.NumField(); i++ {
|
|
vName := vType.Field(i).Name
|
|
field := v.Field(i)
|
|
if strings.EqualFold(name, vName) {
|
|
if !field.CanSet() {
|
|
Errorf(nil, "Can't set Feature %q", name)
|
|
} else {
|
|
zero := reflect.Zero(field.Type())
|
|
field.Set(zero)
|
|
Debugf(nil, "Reset feature %q", name)
|
|
}
|
|
}
|
|
}
|
|
return ft
|
|
}
|
|
|
|
// List returns a slice of all the possible feature names
|
|
func (ft *Features) List() (out []string) {
|
|
v := reflect.ValueOf(ft).Elem()
|
|
vType := v.Type()
|
|
for i := 0; i < v.NumField(); i++ {
|
|
out = append(out, vType.Field(i).Name)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Enabled returns a map of features with keys showing whether they
|
|
// are enabled or not
|
|
func (ft *Features) Enabled() (features map[string]bool) {
|
|
v := reflect.ValueOf(ft).Elem()
|
|
vType := v.Type()
|
|
features = make(map[string]bool, v.NumField())
|
|
for i := 0; i < v.NumField(); i++ {
|
|
vName := vType.Field(i).Name
|
|
field := v.Field(i)
|
|
if field.Kind() == reflect.Func {
|
|
// Can't compare functions
|
|
features[vName] = !field.IsNil()
|
|
} else {
|
|
zero := reflect.Zero(field.Type())
|
|
features[vName] = field.Interface() != zero.Interface()
|
|
}
|
|
}
|
|
return features
|
|
}
|
|
|
|
// DisableList nil's out the comma separated list of named features.
|
|
// If it isn't found then it will log a message.
|
|
func (ft *Features) DisableList(list []string) *Features {
|
|
for _, feature := range list {
|
|
ft.Disable(strings.TrimSpace(feature))
|
|
}
|
|
return ft
|
|
}
|
|
|
|
// Fill fills in the function pointers in the Features struct from the
|
|
// optional interfaces. It returns the original updated Features
|
|
// struct passed in.
|
|
func (ft *Features) Fill(ctx context.Context, f Fs) *Features {
|
|
if do, ok := f.(Purger); ok {
|
|
ft.Purge = do.Purge
|
|
}
|
|
if do, ok := f.(Copier); ok {
|
|
ft.Copy = do.Copy
|
|
}
|
|
if do, ok := f.(Mover); ok {
|
|
ft.Move = do.Move
|
|
}
|
|
if do, ok := f.(DirMover); ok {
|
|
ft.DirMove = do.DirMove
|
|
}
|
|
if do, ok := f.(ChangeNotifier); ok {
|
|
ft.ChangeNotify = do.ChangeNotify
|
|
}
|
|
if do, ok := f.(UnWrapper); ok {
|
|
ft.UnWrap = do.UnWrap
|
|
}
|
|
if do, ok := f.(Wrapper); ok {
|
|
ft.WrapFs = do.WrapFs
|
|
ft.SetWrapper = do.SetWrapper
|
|
}
|
|
if do, ok := f.(DirCacheFlusher); ok {
|
|
ft.DirCacheFlush = do.DirCacheFlush
|
|
}
|
|
if do, ok := f.(PublicLinker); ok {
|
|
ft.PublicLink = do.PublicLink
|
|
}
|
|
if do, ok := f.(PutUncheckeder); ok {
|
|
ft.PutUnchecked = do.PutUnchecked
|
|
}
|
|
if do, ok := f.(PutStreamer); ok {
|
|
ft.PutStream = do.PutStream
|
|
}
|
|
if do, ok := f.(MergeDirser); ok {
|
|
ft.MergeDirs = do.MergeDirs
|
|
}
|
|
if do, ok := f.(CleanUpper); ok {
|
|
ft.CleanUp = do.CleanUp
|
|
}
|
|
if do, ok := f.(ListRer); ok {
|
|
ft.ListR = do.ListR
|
|
}
|
|
if do, ok := f.(Abouter); ok {
|
|
ft.About = do.About
|
|
}
|
|
if do, ok := f.(OpenWriterAter); ok {
|
|
ft.OpenWriterAt = do.OpenWriterAt
|
|
}
|
|
if do, ok := f.(UserInfoer); ok {
|
|
ft.UserInfo = do.UserInfo
|
|
}
|
|
if do, ok := f.(Disconnecter); ok {
|
|
ft.Disconnect = do.Disconnect
|
|
}
|
|
if do, ok := f.(Commander); ok {
|
|
ft.Command = do.Command
|
|
}
|
|
if do, ok := f.(Shutdowner); ok {
|
|
ft.Shutdown = do.Shutdown
|
|
}
|
|
return ft.DisableList(GetConfig(ctx).DisableFeatures)
|
|
}
|
|
|
|
// Mask the Features with the Fs passed in
|
|
//
|
|
// Only optional features which are implemented in both the original
|
|
// Fs AND the one passed in will be advertised. Any features which
|
|
// aren't in both will be set to false/nil, except for UnWrap/Wrap which
|
|
// will be left untouched.
|
|
func (ft *Features) Mask(ctx context.Context, f Fs) *Features {
|
|
mask := f.Features()
|
|
ft.CaseInsensitive = ft.CaseInsensitive && mask.CaseInsensitive
|
|
ft.DuplicateFiles = ft.DuplicateFiles && mask.DuplicateFiles
|
|
ft.ReadMimeType = ft.ReadMimeType && mask.ReadMimeType
|
|
ft.WriteMimeType = ft.WriteMimeType && mask.WriteMimeType
|
|
ft.CanHaveEmptyDirectories = ft.CanHaveEmptyDirectories && mask.CanHaveEmptyDirectories
|
|
ft.BucketBased = ft.BucketBased && mask.BucketBased
|
|
ft.BucketBasedRootOK = ft.BucketBasedRootOK && mask.BucketBasedRootOK
|
|
ft.SetTier = ft.SetTier && mask.SetTier
|
|
ft.GetTier = ft.GetTier && mask.GetTier
|
|
ft.ServerSideAcrossConfigs = ft.ServerSideAcrossConfigs && mask.ServerSideAcrossConfigs
|
|
// ft.IsLocal = ft.IsLocal && mask.IsLocal Don't propagate IsLocal
|
|
ft.SlowModTime = ft.SlowModTime && mask.SlowModTime
|
|
ft.SlowHash = ft.SlowHash && mask.SlowHash
|
|
|
|
if mask.Purge == nil {
|
|
ft.Purge = nil
|
|
}
|
|
if mask.Copy == nil {
|
|
ft.Copy = nil
|
|
}
|
|
if mask.Move == nil {
|
|
ft.Move = nil
|
|
}
|
|
if mask.DirMove == nil {
|
|
ft.DirMove = nil
|
|
}
|
|
if mask.ChangeNotify == nil {
|
|
ft.ChangeNotify = nil
|
|
}
|
|
// if mask.UnWrap == nil {
|
|
// ft.UnWrap = nil
|
|
// }
|
|
// if mask.Wrapper == nil {
|
|
// ft.Wrapper = nil
|
|
// }
|
|
if mask.DirCacheFlush == nil {
|
|
ft.DirCacheFlush = nil
|
|
}
|
|
if mask.PublicLink == nil {
|
|
ft.PublicLink = nil
|
|
}
|
|
if mask.PutUnchecked == nil {
|
|
ft.PutUnchecked = nil
|
|
}
|
|
if mask.PutStream == nil {
|
|
ft.PutStream = nil
|
|
}
|
|
if mask.MergeDirs == nil {
|
|
ft.MergeDirs = nil
|
|
}
|
|
if mask.CleanUp == nil {
|
|
ft.CleanUp = nil
|
|
}
|
|
if mask.ListR == nil {
|
|
ft.ListR = nil
|
|
}
|
|
if mask.About == nil {
|
|
ft.About = nil
|
|
}
|
|
if mask.OpenWriterAt == nil {
|
|
ft.OpenWriterAt = nil
|
|
}
|
|
if mask.UserInfo == nil {
|
|
ft.UserInfo = nil
|
|
}
|
|
if mask.Disconnect == nil {
|
|
ft.Disconnect = nil
|
|
}
|
|
// Command is always local so we don't mask it
|
|
if mask.Shutdown == nil {
|
|
ft.Shutdown = nil
|
|
}
|
|
return ft.DisableList(GetConfig(ctx).DisableFeatures)
|
|
}
|
|
|
|
// Wrap makes a Copy of the features passed in, overriding the UnWrap/Wrap
|
|
// method only if available in f.
|
|
func (ft *Features) Wrap(f Fs) *Features {
|
|
ftCopy := new(Features)
|
|
*ftCopy = *ft
|
|
if do, ok := f.(UnWrapper); ok {
|
|
ftCopy.UnWrap = do.UnWrap
|
|
}
|
|
if do, ok := f.(Wrapper); ok {
|
|
ftCopy.WrapFs = do.WrapFs
|
|
ftCopy.SetWrapper = do.SetWrapper
|
|
}
|
|
return ftCopy
|
|
}
|
|
|
|
// WrapsFs adds extra information between `f` which wraps `w`
|
|
func (ft *Features) WrapsFs(f Fs, w Fs) *Features {
|
|
wFeatures := w.Features()
|
|
if wFeatures.WrapFs != nil && wFeatures.SetWrapper != nil {
|
|
wFeatures.SetWrapper(f)
|
|
}
|
|
return ft
|
|
}
|
|
|
|
// Purger is an optional interfaces for Fs
|
|
type Purger interface {
|
|
// Purge all files in the directory specified
|
|
//
|
|
// 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
|
|
Purge(ctx context.Context, dir string) error
|
|
}
|
|
|
|
// Copier is an optional interface for Fs
|
|
type Copier interface {
|
|
// 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
|
|
Copy(ctx context.Context, src Object, remote string) (Object, error)
|
|
}
|
|
|
|
// Mover is an optional interface for Fs
|
|
type Mover interface {
|
|
// 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
|
|
Move(ctx context.Context, src Object, remote string) (Object, error)
|
|
}
|
|
|
|
// DirMover is an optional interface for Fs
|
|
type DirMover interface {
|
|
// 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
|
|
DirMove(ctx context.Context, src Fs, srcRemote, dstRemote string) error
|
|
}
|
|
|
|
// ChangeNotifier is an optional interface for Fs
|
|
type ChangeNotifier interface {
|
|
// ChangeNotify calls the passed function with a path
|
|
// that has had changes. If the implementation
|
|
// uses polling, it should adhere to the given interval.
|
|
// At least one value will be written to the channel,
|
|
// specifying the initial value and updated values might
|
|
// follow. A 0 Duration should pause the polling.
|
|
// The ChangeNotify implementation must empty the channel
|
|
// regularly. When the channel gets closed, the implementation
|
|
// should stop polling and release resources.
|
|
ChangeNotify(context.Context, func(string, EntryType), <-chan time.Duration)
|
|
}
|
|
|
|
// UnWrapper is an optional interfaces for Fs
|
|
type UnWrapper interface {
|
|
// UnWrap returns the Fs that this Fs is wrapping
|
|
UnWrap() Fs
|
|
}
|
|
|
|
// Wrapper is an optional interfaces for Fs
|
|
type Wrapper interface {
|
|
// Wrap returns the Fs that is wrapping this Fs
|
|
WrapFs() Fs
|
|
// SetWrapper sets the Fs that is wrapping this Fs
|
|
SetWrapper(f Fs)
|
|
}
|
|
|
|
// DirCacheFlusher is an optional interface for Fs
|
|
type DirCacheFlusher interface {
|
|
// DirCacheFlush resets the directory cache - used in testing
|
|
// as an optional interface
|
|
DirCacheFlush()
|
|
}
|
|
|
|
// PutUncheckeder is an optional interface for Fs
|
|
type PutUncheckeder interface {
|
|
// 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
|
|
//
|
|
// May create duplicates or return errors if src already
|
|
// exists.
|
|
PutUnchecked(ctx context.Context, in io.Reader, src ObjectInfo, options ...OpenOption) (Object, error)
|
|
}
|
|
|
|
// PutStreamer is an optional interface for Fs
|
|
type PutStreamer interface {
|
|
// PutStream uploads to the remote path with the modTime given of indeterminate 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
|
|
PutStream(ctx context.Context, in io.Reader, src ObjectInfo, options ...OpenOption) (Object, error)
|
|
}
|
|
|
|
// PublicLinker is an optional interface for Fs
|
|
type PublicLinker interface {
|
|
// PublicLink generates a public link to the remote path (usually readable by anyone)
|
|
PublicLink(ctx context.Context, remote string, expire Duration, unlink bool) (string, error)
|
|
}
|
|
|
|
// MergeDirser is an option interface for Fs
|
|
type MergeDirser interface {
|
|
// MergeDirs merges the contents of all the directories passed
|
|
// in into the first one and rmdirs the other directories.
|
|
MergeDirs(ctx context.Context, dirs []Directory) error
|
|
}
|
|
|
|
// CleanUpper is an optional interfaces for Fs
|
|
type CleanUpper interface {
|
|
// 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.
|
|
CleanUp(ctx context.Context) error
|
|
}
|
|
|
|
// ListRer is an optional interfaces for Fs
|
|
type ListRer interface {
|
|
// 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.
|
|
ListR(ctx context.Context, dir string, callback ListRCallback) error
|
|
}
|
|
|
|
// RangeSeeker is the interface that wraps the RangeSeek method.
|
|
//
|
|
// Some of the returns from Object.Open() may optionally implement
|
|
// this method for efficiency purposes.
|
|
type RangeSeeker interface {
|
|
// RangeSeek behaves like a call to Seek(offset int64, whence
|
|
// int) with the output wrapped in an io.LimitedReader
|
|
// limiting the total length to limit.
|
|
//
|
|
// RangeSeek with a limit of < 0 is equivalent to a regular Seek.
|
|
RangeSeek(ctx context.Context, offset int64, whence int, length int64) (int64, error)
|
|
}
|
|
|
|
// Abouter is an optional interface for Fs
|
|
type Abouter interface {
|
|
// About gets quota information from the Fs
|
|
About(ctx context.Context) (*Usage, error)
|
|
}
|
|
|
|
// OpenWriterAter is an optional interface for Fs
|
|
type OpenWriterAter interface {
|
|
// OpenWriterAt opens with a handle for random access writes
|
|
//
|
|
// Pass in the remote desired and the size if known.
|
|
//
|
|
// It truncates any existing object
|
|
OpenWriterAt(ctx context.Context, remote string, size int64) (WriterAtCloser, error)
|
|
}
|
|
|
|
// UserInfoer is an optional interface for Fs
|
|
type UserInfoer interface {
|
|
// UserInfo returns info about the connected user
|
|
UserInfo(ctx context.Context) (map[string]string, error)
|
|
}
|
|
|
|
// Disconnecter is an optional interface for Fs
|
|
type Disconnecter interface {
|
|
// Disconnect the current user
|
|
Disconnect(ctx context.Context) error
|
|
}
|
|
|
|
// CommandHelp describes a single backend Command
|
|
//
|
|
// These are automatically inserted in the docs
|
|
type CommandHelp struct {
|
|
Name string // Name of the command, e.g. "link"
|
|
Short string // Single line description
|
|
Long string // Long multi-line description
|
|
Opts map[string]string // maps option name to a single line help
|
|
}
|
|
|
|
// Commander is an interface to wrap the Command function
|
|
type Commander interface {
|
|
// Command the backend to run a named command
|
|
//
|
|
// The command run is name
|
|
// args may be used to read arguments from
|
|
// opts may be used to read optional arguments from
|
|
//
|
|
// The result should be capable of being JSON encoded
|
|
// If it is a string or a []string it will be shown to the user
|
|
// otherwise it will be JSON encoded and shown to the user like that
|
|
Command(ctx context.Context, name string, arg []string, opt map[string]string) (interface{}, error)
|
|
}
|
|
|
|
// Shutdowner is an interface to wrap the Shutdown function
|
|
type Shutdowner interface {
|
|
// Shutdown the backend, closing any background tasks and any
|
|
// cached connections.
|
|
Shutdown(ctx context.Context) error
|
|
}
|
|
|
|
// ObjectsChan is a channel of Objects
|
|
type ObjectsChan chan Object
|
|
|
|
// Objects is a slice of Object~s
|
|
type Objects []Object
|
|
|
|
// ObjectPair is a pair of Objects used to describe a potential copy
|
|
// operation.
|
|
type ObjectPair struct {
|
|
Src, Dst Object
|
|
}
|
|
|
|
// UnWrapFs unwraps f as much as possible and returns the base Fs
|
|
func UnWrapFs(f Fs) Fs {
|
|
for {
|
|
unwrap := f.Features().UnWrap
|
|
if unwrap == nil {
|
|
break // not a wrapped Fs, use current
|
|
}
|
|
next := unwrap()
|
|
if next == nil {
|
|
break // no base Fs found, use current
|
|
}
|
|
f = next
|
|
}
|
|
return f
|
|
}
|
|
|
|
// UnWrapObject unwraps o as much as possible and returns the base object
|
|
func UnWrapObject(o Object) Object {
|
|
for {
|
|
u, ok := o.(ObjectUnWrapper)
|
|
if !ok {
|
|
break // not a wrapped object, use current
|
|
}
|
|
next := u.UnWrap()
|
|
if next == nil {
|
|
break // no base object found, use current
|
|
}
|
|
o = next
|
|
}
|
|
return o
|
|
}
|
|
|
|
// UnWrapObjectInfo returns the underlying Object unwrapped as much as
|
|
// possible or nil.
|
|
func UnWrapObjectInfo(oi ObjectInfo) Object {
|
|
o, ok := oi.(Object)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return UnWrapObject(o)
|
|
}
|
|
|
|
// Find looks for a RegInfo object for the name passed in. The name
|
|
// can be either the Name or the Prefix.
|
|
//
|
|
// Services are looked up in the config file
|
|
func Find(name string) (*RegInfo, error) {
|
|
for _, item := range Registry {
|
|
if item.Name == name || item.Prefix == name || item.FileName() == name {
|
|
return item, nil
|
|
}
|
|
}
|
|
return nil, errors.Errorf("didn't find backend called %q", name)
|
|
}
|
|
|
|
// MustFind looks for an Info object for the type name passed in
|
|
//
|
|
// Services are looked up in the config file
|
|
//
|
|
// Exits with a fatal error if not found
|
|
func MustFind(name string) *RegInfo {
|
|
fs, err := Find(name)
|
|
if err != nil {
|
|
log.Fatalf("Failed to find remote: %v", err)
|
|
}
|
|
return fs
|
|
}
|
|
|
|
// ParseRemote deconstructs a path into configName, fsPath, looking up
|
|
// the fsName in the config file (returning NotFoundInConfigFile if not found)
|
|
func ParseRemote(path string) (fsInfo *RegInfo, configName, fsPath string, connectionStringConfig configmap.Simple, err error) {
|
|
parsed, err := fspath.Parse(path)
|
|
if err != nil {
|
|
return nil, "", "", nil, err
|
|
}
|
|
configName, fsPath = parsed.Name, parsed.Path
|
|
var fsName string
|
|
var ok bool
|
|
if configName != "" {
|
|
if strings.HasPrefix(configName, ":") {
|
|
fsName = configName[1:]
|
|
} else {
|
|
m := ConfigMap(nil, configName, parsed.Config)
|
|
fsName, ok = m.Get("type")
|
|
if !ok {
|
|
return nil, "", "", nil, ErrorNotFoundInConfigFile
|
|
}
|
|
}
|
|
} else {
|
|
fsName = "local"
|
|
configName = "local"
|
|
}
|
|
fsInfo, err = Find(fsName)
|
|
return fsInfo, configName, fsPath, parsed.Config, err
|
|
}
|
|
|
|
// A configmap.Getter to read from the environment RCLONE_CONFIG_backend_option_name
|
|
type configEnvVars string
|
|
|
|
// Get a config item from the environment variables if possible
|
|
func (configName configEnvVars) Get(key string) (value string, ok bool) {
|
|
return os.LookupEnv(ConfigToEnv(string(configName), key))
|
|
}
|
|
|
|
// A configmap.Getter to read from the environment RCLONE_option_name
|
|
type optionEnvVars struct {
|
|
fsInfo *RegInfo
|
|
}
|
|
|
|
// Get a config item from the option environment variables if possible
|
|
func (oev optionEnvVars) Get(key string) (value string, ok bool) {
|
|
opt := oev.fsInfo.Options.Get(key)
|
|
if opt == nil {
|
|
return "", false
|
|
}
|
|
// For options with NoPrefix set, check without prefix too
|
|
if opt.NoPrefix {
|
|
value, ok = os.LookupEnv(OptionToEnv(key))
|
|
if ok {
|
|
return value, ok
|
|
}
|
|
}
|
|
return os.LookupEnv(OptionToEnv(oev.fsInfo.Prefix + "-" + key))
|
|
}
|
|
|
|
// A configmap.Getter to read either the default value or the set
|
|
// value from the RegInfo.Options
|
|
type regInfoValues struct {
|
|
fsInfo *RegInfo
|
|
useDefault bool
|
|
}
|
|
|
|
// override the values in configMap with the either the flag values or
|
|
// the default values
|
|
func (r *regInfoValues) Get(key string) (value string, ok bool) {
|
|
opt := r.fsInfo.Options.Get(key)
|
|
if opt != nil && (r.useDefault || opt.Value != nil) {
|
|
return opt.String(), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// A configmap.Setter to read from the config file
|
|
type setConfigFile string
|
|
|
|
// Set a config item into the config file
|
|
func (section setConfigFile) Set(key, value string) {
|
|
if strings.HasPrefix(string(section), ":") {
|
|
Logf(nil, "Can't save config %q = %q for on the fly backend %q", key, value, section)
|
|
return
|
|
}
|
|
Debugf(nil, "Saving config %q = %q in section %q of the config file", key, value, section)
|
|
err := ConfigFileSet(string(section), key, value)
|
|
if err != nil {
|
|
Errorf(nil, "Failed saving config %q = %q in section %q of the config file: %v", key, value, section, err)
|
|
}
|
|
}
|
|
|
|
// A configmap.Getter to read from the config file
|
|
type getConfigFile string
|
|
|
|
// Get a config item from the config file
|
|
func (section getConfigFile) Get(key string) (value string, ok bool) {
|
|
value, ok = ConfigFileGet(string(section), key)
|
|
// Ignore empty lines in the config file
|
|
if value == "" {
|
|
ok = false
|
|
}
|
|
return value, ok
|
|
}
|
|
|
|
// ConfigMap creates a configmap.Map from the *RegInfo and the
|
|
// configName passed in. If connectionStringConfig has any entries (it may be nil),
|
|
// then it will be added to the lookup with the highest priority.
|
|
//
|
|
// If fsInfo is nil then the returned configmap.Map should only be
|
|
// used for reading non backend specific parameters, such as "type".
|
|
func ConfigMap(fsInfo *RegInfo, configName string, connectionStringConfig configmap.Simple) (config *configmap.Map) {
|
|
// Create the config
|
|
config = configmap.New()
|
|
|
|
// Read the config, more specific to least specific
|
|
|
|
// Config from connection string
|
|
if len(connectionStringConfig) > 0 {
|
|
config.AddGetter(connectionStringConfig, configmap.PriorityNormal)
|
|
}
|
|
|
|
// flag values
|
|
if fsInfo != nil {
|
|
config.AddGetter(®InfoValues{fsInfo, false}, configmap.PriorityNormal)
|
|
}
|
|
|
|
// remote specific environment vars
|
|
config.AddGetter(configEnvVars(configName), configmap.PriorityNormal)
|
|
|
|
// backend specific environment vars
|
|
if fsInfo != nil {
|
|
config.AddGetter(optionEnvVars{fsInfo: fsInfo}, configmap.PriorityNormal)
|
|
}
|
|
|
|
// config file
|
|
config.AddGetter(getConfigFile(configName), configmap.PriorityConfig)
|
|
|
|
// default values
|
|
if fsInfo != nil {
|
|
config.AddGetter(®InfoValues{fsInfo, true}, configmap.PriorityDefault)
|
|
}
|
|
|
|
// Set Config
|
|
config.AddSetter(setConfigFile(configName))
|
|
return config
|
|
}
|
|
|
|
// ConfigFs makes the config for calling NewFs with.
|
|
//
|
|
// It parses the path which is of the form remote:path
|
|
//
|
|
// Remotes are looked up in the config file. If the remote isn't
|
|
// found then NotFoundInConfigFile will be returned.
|
|
func ConfigFs(path string) (fsInfo *RegInfo, configName, fsPath string, config *configmap.Map, err error) {
|
|
// Parse the remote path
|
|
fsInfo, configName, fsPath, connectionStringConfig, err := ParseRemote(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
config = ConfigMap(fsInfo, configName, connectionStringConfig)
|
|
return
|
|
}
|
|
|
|
// NewFs makes a new Fs object from the path
|
|
//
|
|
// The path is of the form remote:path
|
|
//
|
|
// Remotes are looked up in the config file. If the remote isn't
|
|
// found then NotFoundInConfigFile will be returned.
|
|
//
|
|
// On Windows avoid single character remote names as they can be mixed
|
|
// up with drive letters.
|
|
func NewFs(ctx context.Context, path string) (Fs, error) {
|
|
Debugf(nil, "Creating backend with remote %q", path)
|
|
fsInfo, configName, fsPath, config, err := ConfigFs(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
overridden := fsInfo.Options.Overridden(config)
|
|
if len(overridden) > 0 {
|
|
extraConfig := overridden.String()
|
|
//Debugf(nil, "detected overriden config %q", extraConfig)
|
|
md5sumBinary := md5.Sum([]byte(extraConfig))
|
|
suffix := base64.RawURLEncoding.EncodeToString(md5sumBinary[:])
|
|
// 5 characters length is 5*6 = 30 bits of base64
|
|
const maxLength = 5
|
|
if len(suffix) > maxLength {
|
|
suffix = suffix[:maxLength]
|
|
}
|
|
suffix = "{" + suffix + "}"
|
|
Debugf(configName, "detected overridden config - adding %q suffix to name", suffix)
|
|
// Add the suffix to the config name
|
|
//
|
|
// These need to work as filesystem names as the VFS cache will use them
|
|
configName += suffix
|
|
}
|
|
return fsInfo.NewFs(ctx, configName, fsPath, config)
|
|
}
|
|
|
|
// ConfigString returns a canonical version of the config string used
|
|
// to configure the Fs as passed to fs.NewFs
|
|
func ConfigString(f Fs) string {
|
|
name := f.Name()
|
|
root := f.Root()
|
|
if name == "local" && f.Features().IsLocal {
|
|
return root
|
|
}
|
|
return name + ":" + root
|
|
}
|
|
|
|
// TemporaryLocalFs creates a local FS in the OS's temporary directory.
|
|
//
|
|
// No cleanup is performed, the caller must call Purge on the Fs themselves.
|
|
func TemporaryLocalFs(ctx context.Context) (Fs, error) {
|
|
path, err := ioutil.TempDir("", "rclone-spool")
|
|
if err == nil {
|
|
err = os.Remove(path)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
path = filepath.ToSlash(path)
|
|
return NewFs(ctx, path)
|
|
}
|
|
|
|
// CheckClose is a utility function used to check the return from
|
|
// Close in a defer statement.
|
|
func CheckClose(c io.Closer, err *error) {
|
|
cerr := c.Close()
|
|
if *err == nil {
|
|
*err = cerr
|
|
}
|
|
}
|
|
|
|
// FileExists returns true if a file remote exists.
|
|
// If remote is a directory, FileExists returns false.
|
|
func FileExists(ctx context.Context, fs Fs, remote string) (bool, error) {
|
|
_, err := fs.NewObject(ctx, remote)
|
|
if err != nil {
|
|
if err == ErrorObjectNotFound || err == ErrorNotAFile || err == ErrorPermissionDenied {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// GetModifyWindow calculates the maximum modify window between the given Fses
|
|
// and the Config.ModifyWindow parameter.
|
|
func GetModifyWindow(ctx context.Context, fss ...Info) time.Duration {
|
|
window := GetConfig(ctx).ModifyWindow
|
|
for _, f := range fss {
|
|
if f != nil {
|
|
precision := f.Precision()
|
|
if precision == ModTimeNotSupported {
|
|
return ModTimeNotSupported
|
|
}
|
|
if precision > window {
|
|
window = precision
|
|
}
|
|
}
|
|
}
|
|
return window
|
|
}
|
|
|
|
// Pacer is a simple wrapper around a pacer.Pacer with logging.
|
|
type Pacer struct {
|
|
*pacer.Pacer
|
|
}
|
|
|
|
type logCalculator struct {
|
|
pacer.Calculator
|
|
}
|
|
|
|
// NewPacer creates a Pacer for the given Fs and Calculator.
|
|
func NewPacer(ctx context.Context, c pacer.Calculator) *Pacer {
|
|
ci := GetConfig(ctx)
|
|
retries := ci.LowLevelRetries
|
|
if retries <= 0 {
|
|
retries = 1
|
|
}
|
|
p := &Pacer{
|
|
Pacer: pacer.New(
|
|
pacer.InvokerOption(pacerInvoker),
|
|
pacer.MaxConnectionsOption(ci.Checkers+ci.Transfers),
|
|
pacer.RetriesOption(retries),
|
|
pacer.CalculatorOption(c),
|
|
),
|
|
}
|
|
p.SetCalculator(c)
|
|
return p
|
|
}
|
|
|
|
func (d *logCalculator) Calculate(state pacer.State) time.Duration {
|
|
oldSleepTime := state.SleepTime
|
|
newSleepTime := d.Calculator.Calculate(state)
|
|
if state.ConsecutiveRetries > 0 {
|
|
if newSleepTime != oldSleepTime {
|
|
Debugf("pacer", "Rate limited, increasing sleep to %v", newSleepTime)
|
|
}
|
|
} else {
|
|
if newSleepTime != oldSleepTime {
|
|
Debugf("pacer", "Reducing sleep to %v", newSleepTime)
|
|
}
|
|
}
|
|
return newSleepTime
|
|
}
|
|
|
|
// SetCalculator sets the pacing algorithm. Don't modify the Calculator object
|
|
// afterwards, use the ModifyCalculator method when needed.
|
|
//
|
|
// It will choose the default algorithm if nil is passed in.
|
|
func (p *Pacer) SetCalculator(c pacer.Calculator) {
|
|
switch c.(type) {
|
|
case *logCalculator:
|
|
Logf("pacer", "Invalid Calculator in fs.Pacer.SetCalculator")
|
|
case nil:
|
|
c = &logCalculator{pacer.NewDefault()}
|
|
default:
|
|
c = &logCalculator{c}
|
|
}
|
|
|
|
p.Pacer.SetCalculator(c)
|
|
}
|
|
|
|
// ModifyCalculator calls the given function with the currently configured
|
|
// Calculator and the Pacer lock held.
|
|
func (p *Pacer) ModifyCalculator(f func(pacer.Calculator)) {
|
|
p.ModifyCalculator(func(c pacer.Calculator) {
|
|
switch _c := c.(type) {
|
|
case *logCalculator:
|
|
f(_c.Calculator)
|
|
default:
|
|
Logf("pacer", "Invalid Calculator in fs.Pacer: %t", c)
|
|
f(c)
|
|
}
|
|
})
|
|
}
|
|
|
|
func pacerInvoker(try, retries int, f pacer.Paced) (retry bool, err error) {
|
|
retry, err = f()
|
|
if retry {
|
|
Debugf("pacer", "low level retry %d/%d (error %v)", try, retries, err)
|
|
err = fserrors.RetryError(err)
|
|
}
|
|
return
|
|
}
|