mirror of
https://github.com/rclone/rclone.git
synced 2025-07-20 05:43:42 +02:00
This is a behavior-preserving refactor. I'm mostly just moving the code that defines and parses configs (e.g. "rcloneremotename") into a new source file. This lets us focus more on implementing the text protocol in gitannex.go.
132 lines
4.5 KiB
Go
132 lines
4.5 KiB
Go
package gitannex
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/config"
|
|
"github.com/rclone/rclone/fs/fspath"
|
|
)
|
|
|
|
type configID int
|
|
|
|
const (
|
|
configRemoteName configID = iota
|
|
configPrefix
|
|
configLayout
|
|
)
|
|
|
|
// configDefinition describes a configuration value required by this command. We
|
|
// use "GETCONFIG" messages to query git-annex for these values at runtime.
|
|
type configDefinition struct {
|
|
id configID
|
|
names []string
|
|
description string
|
|
defaultValue string
|
|
}
|
|
|
|
const (
|
|
defaultRclonePrefix = "git-annex-rclone"
|
|
defaultRcloneLayout = "nodir"
|
|
)
|
|
|
|
var requiredConfigs = []configDefinition{
|
|
{
|
|
id: configRemoteName,
|
|
names: []string{"rcloneremotename", "target"},
|
|
description: "Name of the rclone remote to use. " +
|
|
"Must match a remote known to rclone. " +
|
|
"(Note that rclone remotes are a distinct concept from git-annex remotes.)",
|
|
},
|
|
{
|
|
id: configPrefix,
|
|
names: []string{"rcloneprefix", "prefix"},
|
|
description: "Directory where rclone will write git-annex content. " +
|
|
fmt.Sprintf("If not specified, defaults to %q. ", defaultRclonePrefix) +
|
|
"This directory will be created on init if it does not exist.",
|
|
defaultValue: defaultRclonePrefix,
|
|
},
|
|
{
|
|
id: configLayout,
|
|
names: []string{"rclonelayout", "rclone_layout"},
|
|
description: "Defines where, within the rcloneprefix directory, rclone will write git-annex content. " +
|
|
fmt.Sprintf("Must be one of %v. ", allLayoutModes()) +
|
|
fmt.Sprintf("If empty, defaults to %q.", defaultRcloneLayout),
|
|
defaultValue: defaultRcloneLayout,
|
|
},
|
|
}
|
|
|
|
func (c *configDefinition) getCanonicalName() string {
|
|
if len(c.names) < 1 {
|
|
panic(fmt.Errorf("configDefinition must have at least one name: %v", c))
|
|
}
|
|
return c.names[0]
|
|
}
|
|
|
|
// fullDescription returns a single-line, human-readable description for this
|
|
// config. The returned string begins with a list of synonyms and ends with
|
|
// `c.description`.
|
|
func (c *configDefinition) fullDescription() string {
|
|
if len(c.names) <= 1 {
|
|
return c.description
|
|
}
|
|
// Exclude the canonical name from the list of synonyms.
|
|
synonyms := c.names[1:len(c.names)]
|
|
commaSeparatedSynonyms := strings.Join(synonyms, ", ")
|
|
return fmt.Sprintf("(synonyms: %s) %s", commaSeparatedSynonyms, c.description)
|
|
}
|
|
|
|
// validateRemoteName validates the "rcloneremotename" config that we receive
|
|
// from git-annex. It returns nil iff `value` is valid. Otherwise, it returns a
|
|
// descriptive error suitable for sending back to git-annex via stdout.
|
|
//
|
|
// The value is only valid when:
|
|
// 1. It is the exact name of an existing remote.
|
|
// 2. It is an fspath string that names an existing remote or a backend. The
|
|
// string may include options, but it must not include a path. (That's what
|
|
// the "rcloneprefix" config is for.)
|
|
//
|
|
// While backends are not remote names, per se, they are permitted for
|
|
// compatibility with [fstest]. We could guard this behavior behind
|
|
// [testing.Testing] to prevent users from specifying backend strings, but
|
|
// there's no obvious harm in permitting it.
|
|
func validateRemoteName(value string) error {
|
|
remoteNames := config.GetRemoteNames()
|
|
// Check whether `value` is an exact match for an existing remote.
|
|
//
|
|
// If we checked whether [cache.Get] returns [fs.ErrorNotFoundInConfigFile],
|
|
// we would incorrectly identify file names as valid remote names. We also
|
|
// avoid [config.FileSections] because it will miss remotes that are defined
|
|
// by environment variables.
|
|
if slices.Contains(remoteNames, value) {
|
|
return nil
|
|
}
|
|
parsed, err := fspath.Parse(value)
|
|
if err != nil {
|
|
return fmt.Errorf("remote could not be parsed: %s", value)
|
|
}
|
|
if parsed.Path != "" {
|
|
return fmt.Errorf("remote does not exist or incorrectly contains a path: %s", value)
|
|
}
|
|
// Now that we've established `value` is an fspath string that does not
|
|
// include a path component, we only need to check whether it names an
|
|
// existing remote or backend.
|
|
if slices.Contains(remoteNames, parsed.Name) {
|
|
return nil
|
|
}
|
|
maybeBackend := strings.HasPrefix(value, ":")
|
|
if !maybeBackend {
|
|
return fmt.Errorf("remote does not exist: %s", value)
|
|
}
|
|
// Strip the leading colon before searching for the backend. For instance,
|
|
// search for "local" instead of ":local". Note that `parsed.Name` already
|
|
// omits any config options baked into the string.
|
|
trimmedBackendName := strings.TrimPrefix(parsed.Name, ":")
|
|
if _, err = fs.Find(trimmedBackendName); err != nil {
|
|
return fmt.Errorf("backend does not exist: %s", trimmedBackendName)
|
|
}
|
|
return nil
|
|
}
|