mirror of
https://github.com/rclone/rclone
synced 2024-12-19 09:05:56 +01:00
e43b5ce5e5
This is possible now that we no longer support go1.12 and brings rclone into line with standard practices in the Go world. This also removes errors.New and errors.Errorf from lib/errors and prefers the stdlib errors package over lib/errors.
286 lines
7.5 KiB
Go
286 lines
7.5 KiB
Go
package fs
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
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, fmt.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, fmt.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
|
|
}
|