package docker import ( "fmt" "strconv" "strings" "github.com/rclone/rclone/cmd/mountlib" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/fspath" "github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/vfs/vfscommon" "github.com/rclone/rclone/vfs/vfsflags" "github.com/spf13/pflag" ) // applyOptions configures volume from request options. // // There are 5 special options: // - "remote" aka "fs" determines existing remote from config file // with a path or on-the-fly remote using the ":backend:" syntax. // It is usually named "remote" in documentation but can be aliased as // "fs" to avoid confusion with the "remote" option of some backends. // - "type" is equivalent to the ":backend:" syntax (optional). // - "path" provides explicit on-remote path for "type" (optional). // - "mount-type" can be "mount", "cmount" or "mount2", defaults to // first found (optional). // - "persist" is reserved for future to create remotes persisted // in rclone.conf similar to rcd (optional). // // Unlike rcd we use the flat naming scheme for mount, vfs and backend // options without substructures. Dashes, underscores and mixed case // in option names can be used interchangeably. Option name conflicts // can be resolved in a manner similar to rclone CLI by adding prefixes: // "vfs-", primary mount backend type like "sftp-", and so on. // // After triaging the options are put in MountOpt, VFSOpt or connect // string for actual filesystem setup and in volume.Options for saving // the state. func (vol *Volume) applyOptions(volOpt VolOpts) error { // copy options to override later mntOpt := &vol.mnt.MountOpt vfsOpt := &vol.mnt.VFSOpt *mntOpt = vol.drv.mntOpt *vfsOpt = vol.drv.vfsOpt // vol.Options has all options except "remote" and "type" vol.Options = VolOpts{} vol.fsString = "" var fsName, fsPath, fsType string var explicitPath string var fsOpt configmap.Simple // parse "remote" or "type" for key, str := range volOpt { switch key { case "": continue case "remote", "fs": if str != "" { p, err := fspath.Parse(str) if err != nil || p.Name == ":" { return fmt.Errorf("cannot parse path %q: %w", str, err) } fsName, fsPath, fsOpt = p.Name, p.Path, p.Config vol.Fs = str } case "type": fsType = str vol.Type = str case "path": explicitPath = str vol.Path = str default: vol.Options[key] = str } } // find options supported by backend if strings.HasPrefix(fsName, ":") { fsType = fsName[1:] fsName = "" } if fsType == "" { fsType = "local" if fsName != "" { var ok bool fsType, ok = fs.ConfigMap(nil, fsName, nil).Get("type") if !ok { return fs.ErrorNotFoundInConfigFile } } } if explicitPath != "" { if fsPath != "" { fs.Logf(nil, "Explicit path will override connection string") } fsPath = explicitPath } fsInfo, err := fs.Find(fsType) if err != nil { return fmt.Errorf("unknown filesystem type %q", fsType) } // handle remaining options, override fsOpt if fsOpt == nil { fsOpt = configmap.Simple{} } opt := rc.Params{} for key, val := range vol.Options { opt[key] = val } for key := range opt { var ok bool var err error switch normalOptName(key) { case "persist": vol.persist, err = opt.GetBool(key) ok = true case "mount-type": vol.mountType, err = opt.GetString(key) ok = true } if err != nil { return fmt.Errorf("cannot parse option %q: %w", key, err) } if !ok { // try to use as a mount option in mntOpt ok, err = getMountOption(mntOpt, opt, key) if ok && err != nil { return fmt.Errorf("cannot parse mount option %q: %w", key, err) } } if !ok { // try as a vfs option in vfsOpt ok, err = getVFSOption(vfsOpt, opt, key) if ok && err != nil { return fmt.Errorf("cannot parse vfs option %q: %w", key, err) } } if !ok { // try as a backend option in fsOpt (backends use "_" instead of "-") optWithPrefix := strings.ReplaceAll(normalOptName(key), "-", "_") fsOptName := strings.TrimPrefix(optWithPrefix, fsType+"_") hasFsPrefix := optWithPrefix != fsOptName if !hasFsPrefix || fsInfo.Options.Get(fsOptName) == nil { fs.Logf(nil, "Option %q is not supported by backend %q", key, fsType) return fmt.Errorf("unsupported backend option %q", key) } fsOpt[fsOptName], err = opt.GetString(key) if err != nil { return fmt.Errorf("cannot parse backend option %q: %w", key, err) } } } // build remote string from fsName, fsType, fsOpt, fsPath colon := ":" comma := "," if fsName == "" { fsName = ":" + fsType } connString := fsOpt.String() if fsName == "" && fsType == "" { colon = "" connString = "" } if connString == "" { comma = "" } vol.fsString = fsName + comma + connString + colon + fsPath return vol.validate() } func getMountOption(mntOpt *mountlib.Options, opt rc.Params, key string) (ok bool, err error) { ok = true switch normalOptName(key) { case "debug-fuse": mntOpt.DebugFUSE, err = opt.GetBool(key) case "attr-timeout": mntOpt.AttrTimeout, err = opt.GetDuration(key) case "option": mntOpt.ExtraOptions, err = getStringArray(opt, key) case "fuse-flag": mntOpt.ExtraFlags, err = getStringArray(opt, key) case "daemon": mntOpt.Daemon, err = opt.GetBool(key) case "daemon-timeout": mntOpt.DaemonTimeout, err = opt.GetDuration(key) case "default-permissions": mntOpt.DefaultPermissions, err = opt.GetBool(key) case "allow-non-empty": mntOpt.AllowNonEmpty, err = opt.GetBool(key) case "allow-root": mntOpt.AllowRoot, err = opt.GetBool(key) case "allow-other": mntOpt.AllowOther, err = opt.GetBool(key) case "async-read": mntOpt.AsyncRead, err = opt.GetBool(key) case "max-read-ahead": err = getFVarP(&mntOpt.MaxReadAhead, opt, key) case "write-back-cache": mntOpt.WritebackCache, err = opt.GetBool(key) case "volname": mntOpt.VolumeName, err = opt.GetString(key) case "noappledouble": mntOpt.NoAppleDouble, err = opt.GetBool(key) case "noapplexattr": mntOpt.NoAppleXattr, err = opt.GetBool(key) case "network-mode": mntOpt.NetworkMode, err = opt.GetBool(key) default: ok = false } return } func getVFSOption(vfsOpt *vfscommon.Options, opt rc.Params, key string) (ok bool, err error) { var intVal int64 ok = true switch normalOptName(key) { // options prefixed with "vfs-" case "vfs-cache-mode": err = getFVarP(&vfsOpt.CacheMode, opt, key) case "vfs-cache-poll-interval": vfsOpt.CachePollInterval, err = opt.GetDuration(key) case "vfs-cache-max-age": vfsOpt.CacheMaxAge, err = opt.GetDuration(key) case "vfs-cache-max-size": err = getFVarP(&vfsOpt.CacheMaxSize, opt, key) case "vfs-read-chunk-size": err = getFVarP(&vfsOpt.ChunkSize, opt, key) case "vfs-read-chunk-size-limit": err = getFVarP(&vfsOpt.ChunkSizeLimit, opt, key) case "vfs-case-insensitive": vfsOpt.CaseInsensitive, err = opt.GetBool(key) case "vfs-write-wait": vfsOpt.WriteWait, err = opt.GetDuration(key) case "vfs-read-wait": vfsOpt.ReadWait, err = opt.GetDuration(key) case "vfs-write-back": vfsOpt.WriteBack, err = opt.GetDuration(key) case "vfs-read-ahead": err = getFVarP(&vfsOpt.ReadAhead, opt, key) case "vfs-used-is-size": vfsOpt.UsedIsSize, err = opt.GetBool(key) // unprefixed vfs options case "no-modtime": vfsOpt.NoModTime, err = opt.GetBool(key) case "no-checksum": vfsOpt.NoChecksum, err = opt.GetBool(key) case "dir-cache-time": vfsOpt.DirCacheTime, err = opt.GetDuration(key) case "poll-interval": vfsOpt.PollInterval, err = opt.GetDuration(key) case "read-only": vfsOpt.ReadOnly, err = opt.GetBool(key) case "dir-perms": perms := &vfsflags.FileMode{Mode: &vfsOpt.DirPerms} err = getFVarP(perms, opt, key) case "file-perms": perms := &vfsflags.FileMode{Mode: &vfsOpt.FilePerms} err = getFVarP(perms, opt, key) // unprefixed unix-only vfs options case "umask": // GetInt64 doesn't support the `0octal` umask syntax - parse locally var strVal string if strVal, err = opt.GetString(key); err == nil { var longVal int64 if longVal, err = strconv.ParseInt(strVal, 0, 0); err == nil { vfsOpt.Umask = int(longVal) } } case "uid": intVal, err = opt.GetInt64(key) vfsOpt.UID = uint32(intVal) case "gid": intVal, err = opt.GetInt64(key) vfsOpt.GID = uint32(intVal) // non-vfs options default: ok = false } return } func getFVarP(pvalue pflag.Value, opt rc.Params, key string) error { str, err := opt.GetString(key) if err != nil { return err } return pvalue.Set(str) } func getStringArray(opt rc.Params, key string) ([]string, error) { str, err := opt.GetString(key) if err != nil { return nil, err } return strings.Split(str, ","), nil } func normalOptName(key string) string { return strings.ReplaceAll(strings.TrimPrefix(strings.ToLower(key), "--"), "_", "-") }