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 }