repo_description: "rsync for cloud storage" - Google Drive, S3, Dropbox, Backblaze B2, One Drive, Swift, Hubic, Wasabi, Google Cloud Storage, Azure Blob, Azure Files, Yandex Files
repo_topics: azure-blob azure-blob-storage azure-files backblaze-b2 cloud-storage dropbox encryption ftp fuse-filesystem go golang google-cloud-storage google-drive onedrive openstack-swift rclone s3 sftp sync webdav
code: 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
  }
  