mirror of
https://github.com/rclone/rclone
synced 2024-11-17 17:30:37 +01:00
287 lines
7.6 KiB
Go
287 lines
7.6 KiB
Go
|
package fs
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"runtime"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
func init() {
|
||
|
// This block is run super-early, before configuration harness kick in
|
||
|
if IsMountHelper() {
|
||
|
if args, err := convertMountHelperArgs(os.Args); err == nil {
|
||
|
os.Args = args
|
||
|
} else {
|
||
|
log.Fatalf("Failed to parse command line: %v", err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// PassDaemonArgsAsEnviron tells how CLI arguments are passed to the daemon
|
||
|
// When false, arguments are passed as is, visible in the `ps` output.
|
||
|
// When true, arguments are converted into environment variables (more secure).
|
||
|
var PassDaemonArgsAsEnviron bool
|
||
|
|
||
|
// Comma-separated list of mount options to ignore.
|
||
|
// Leading and trailing commas are required.
|
||
|
const helperIgnoredOpts = ",rw,_netdev,nofail,user,dev,nodev,suid,nosuid,exec,noexec,auto,noauto,"
|
||
|
|
||
|
// Valid option name characters
|
||
|
const helperValidOptChars = "-_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||
|
|
||
|
// Parser errors
|
||
|
var (
|
||
|
errHelperBadOption = errors.New("option names may only contain `0-9`, `A-Z`, `a-z`, `-` and `_`")
|
||
|
errHelperOptionName = errors.New("option name can't start with `-` or `_`")
|
||
|
errHelperEmptyOption = errors.New("option name can't be empty")
|
||
|
errHelperQuotedValue = errors.New("unterminated quoted value")
|
||
|
errHelperAfterQuote = errors.New("expecting `,` or another quote after a quote")
|
||
|
errHelperSyntax = errors.New("syntax error in option string")
|
||
|
errHelperEmptyCommand = errors.New("command name can't be empty")
|
||
|
errHelperEnvSyntax = errors.New("environment variable must have syntax env.NAME=[VALUE]")
|
||
|
)
|
||
|
|
||
|
// IsMountHelper returns true if rclone was invoked as mount helper:
|
||
|
// as /sbin/mount.rlone (by /bin/mount)
|
||
|
// or /usr/bin/rclonefs (by fusermount or directly)
|
||
|
func IsMountHelper() bool {
|
||
|
if runtime.GOOS == "windows" {
|
||
|
return false
|
||
|
}
|
||
|
me := filepath.Base(os.Args[0])
|
||
|
return me == "mount.rclone" || me == "rclonefs"
|
||
|
}
|
||
|
|
||
|
// convertMountHelperArgs converts "-o" styled mount helper arguments
|
||
|
// into usual rclone flags
|
||
|
func convertMountHelperArgs(origArgs []string) ([]string, error) {
|
||
|
if IsDaemon() {
|
||
|
// The arguments have already been converted by the parent
|
||
|
return origArgs, nil
|
||
|
}
|
||
|
|
||
|
args := []string{}
|
||
|
command := "mount"
|
||
|
parseOpts := false
|
||
|
gotDaemon := false
|
||
|
gotVerbose := false
|
||
|
vCount := 0
|
||
|
|
||
|
for _, arg := range origArgs[1:] {
|
||
|
if !parseOpts {
|
||
|
switch arg {
|
||
|
case "-o", "--opt":
|
||
|
parseOpts = true
|
||
|
case "-v", "-vv", "-vvv", "-vvvv":
|
||
|
vCount += len(arg) - 1
|
||
|
case "-h", "--help":
|
||
|
args = append(args, "--help")
|
||
|
default:
|
||
|
if strings.HasPrefix(arg, "-") {
|
||
|
return nil, errors.Errorf("flag %q is not supported in mount mode", arg)
|
||
|
}
|
||
|
args = append(args, arg)
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
opts, err := parseHelperOptionString(arg)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
parseOpts = false
|
||
|
|
||
|
for _, opt := range opts {
|
||
|
if strings.Contains(helperIgnoredOpts, ","+opt+",") || strings.HasPrefix(opt, "x-systemd") {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
param, value := opt, ""
|
||
|
if idx := strings.Index(opt, "="); idx != -1 {
|
||
|
param, value = opt[:idx], opt[idx+1:]
|
||
|
}
|
||
|
|
||
|
// Set environment variables
|
||
|
if strings.HasPrefix(param, "env.") {
|
||
|
if param = param[4:]; param == "" {
|
||
|
return nil, errHelperEnvSyntax
|
||
|
}
|
||
|
_ = os.Setenv(param, value)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
switch param {
|
||
|
// Change command to run
|
||
|
case "command":
|
||
|
if value == "" {
|
||
|
return nil, errHelperEmptyCommand
|
||
|
}
|
||
|
command = value
|
||
|
continue
|
||
|
// Flag StartDaemon to pass arguments as environment
|
||
|
case "args2env":
|
||
|
PassDaemonArgsAsEnviron = true
|
||
|
continue
|
||
|
// Handle verbosity options
|
||
|
case "v", "vv", "vvv", "vvvv":
|
||
|
vCount += len(param)
|
||
|
continue
|
||
|
case "verbose":
|
||
|
gotVerbose = true
|
||
|
// Don't add --daemon if it was explicitly included
|
||
|
case "daemon":
|
||
|
gotDaemon = true
|
||
|
// Alias for the standard mount option "ro"
|
||
|
case "ro":
|
||
|
param = "read-only"
|
||
|
}
|
||
|
|
||
|
arg = "--" + strings.ToLower(strings.ReplaceAll(param, "_", "-"))
|
||
|
if value != "" {
|
||
|
arg += "=" + value
|
||
|
}
|
||
|
args = append(args, arg)
|
||
|
}
|
||
|
}
|
||
|
if parseOpts {
|
||
|
return nil, errors.Errorf("dangling -o without argument")
|
||
|
}
|
||
|
|
||
|
if vCount > 0 && !gotVerbose {
|
||
|
args = append(args, fmt.Sprintf("--verbose=%d", vCount))
|
||
|
}
|
||
|
if strings.Contains(command, "mount") && !gotDaemon {
|
||
|
// Default to daemonized mount
|
||
|
args = append(args, "--daemon")
|
||
|
}
|
||
|
if len(args) > 0 && args[0] == command {
|
||
|
// Remove artefact of repeated conversion
|
||
|
args = args[1:]
|
||
|
}
|
||
|
prepend := []string{origArgs[0], command}
|
||
|
return append(prepend, args...), nil
|
||
|
}
|
||
|
|
||
|
// parseHelperOptionString deconstructs the -o value into slice of options
|
||
|
// in a way similar to connection strings.
|
||
|
// Example:
|
||
|
// param1=value,param2="qvalue",param3='item1,item2',param4="a ""b"" 'c'"
|
||
|
// An error may be returned if the remote name has invalid characters
|
||
|
// or the parameters are invalid or the path is empty.
|
||
|
//
|
||
|
// The algorithm was adapted from fspath.Parse with some modifications:
|
||
|
// - allow `-` in option names
|
||
|
// - handle special options `x-systemd.X` and `env.X`
|
||
|
// - drop support for :backend: and /path
|
||
|
func parseHelperOptionString(optString string) (opts []string, err error) {
|
||
|
if optString = strings.TrimSpace(optString); optString == "" {
|
||
|
return nil, nil
|
||
|
}
|
||
|
// States for parser
|
||
|
const (
|
||
|
stateParam = uint8(iota)
|
||
|
stateValue
|
||
|
stateQuotedValue
|
||
|
stateAfterQuote
|
||
|
stateDone
|
||
|
)
|
||
|
var (
|
||
|
state = stateParam // current state of parser
|
||
|
i int // position in path
|
||
|
prev int // previous position in path
|
||
|
c rune // current rune under consideration
|
||
|
quote rune // kind of quote to end this quoted string
|
||
|
param string // current parameter value
|
||
|
doubled bool // set if had doubled quotes
|
||
|
)
|
||
|
for i, c = range optString + "," {
|
||
|
switch state {
|
||
|
// Parses param= and param2=
|
||
|
case stateParam:
|
||
|
switch c {
|
||
|
case ',', '=':
|
||
|
param = optString[prev:i]
|
||
|
if len(param) == 0 {
|
||
|
return nil, errHelperEmptyOption
|
||
|
}
|
||
|
if param[0] == '-' || param[0] == '_' {
|
||
|
return nil, errHelperOptionName
|
||
|
}
|
||
|
prev = i + 1
|
||
|
if c == '=' {
|
||
|
state = stateValue
|
||
|
break
|
||
|
}
|
||
|
opts = append(opts, param)
|
||
|
case '.':
|
||
|
if pref := optString[prev:i]; pref != "env" && pref != "x-systemd" {
|
||
|
return nil, errHelperBadOption
|
||
|
}
|
||
|
default:
|
||
|
if !strings.ContainsRune(helperValidOptChars, c) {
|
||
|
return nil, errHelperBadOption
|
||
|
}
|
||
|
}
|
||
|
case stateValue:
|
||
|
switch c {
|
||
|
case '\'', '"':
|
||
|
if i == prev {
|
||
|
quote = c
|
||
|
prev = i + 1
|
||
|
doubled = false
|
||
|
state = stateQuotedValue
|
||
|
}
|
||
|
case ',':
|
||
|
value := optString[prev:i]
|
||
|
prev = i + 1
|
||
|
opts = append(opts, param+"="+value)
|
||
|
state = stateParam
|
||
|
}
|
||
|
case stateQuotedValue:
|
||
|
if c == quote {
|
||
|
state = stateAfterQuote
|
||
|
}
|
||
|
case stateAfterQuote:
|
||
|
switch c {
|
||
|
case ',':
|
||
|
value := optString[prev : i-1]
|
||
|
// replace any doubled quotes if there were any
|
||
|
if doubled {
|
||
|
value = strings.ReplaceAll(value, string(quote)+string(quote), string(quote))
|
||
|
}
|
||
|
prev = i + 1
|
||
|
opts = append(opts, param+"="+value)
|
||
|
state = stateParam
|
||
|
case quote:
|
||
|
// Here is a doubled quote to indicate a literal quote
|
||
|
state = stateQuotedValue
|
||
|
doubled = true
|
||
|
default:
|
||
|
return nil, errHelperAfterQuote
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Depending on which state we were in when we fell off the
|
||
|
// end of the state machine we can return a sensible error.
|
||
|
if state == stateParam && prev > len(optString) {
|
||
|
state = stateDone
|
||
|
}
|
||
|
switch state {
|
||
|
case stateQuotedValue:
|
||
|
return nil, errHelperQuotedValue
|
||
|
case stateAfterQuote:
|
||
|
return nil, errHelperAfterQuote
|
||
|
case stateDone:
|
||
|
break
|
||
|
default:
|
||
|
return nil, errHelperSyntax
|
||
|
}
|
||
|
return opts, nil
|
||
|
}
|