There are three ways to implement a new special remote:

  1. Using the hook special remote to tell git-annex what commands to run to store and retrieve data. This is the easiest way, and is great for prototyping.
  2. Writing it in Haskell and adding it to git-annex.
  3. Writing a program in any language you like that speaks the external special remote protocol.

The rest of this page concentrates on writing new external special remotes. It's not hard!

  • All you need is to make a program with a name like git-annex-remote-$bar.
  • Install it in PATH.
  • When the user runs git annex initremote foo type=external externaltype=$bar, it will use your program.
  • See external special remote protocol for what the program needs to do. There's an example at the end of this page.
  • If things don't seem to work, pass --debug and you'll see, amoung other things, a transcript of git-annex's communication with your program.
  • If you build a new special remote, please add it to the list of special remotes.

Here's an example of using an external special remote to add torrent support to git-annex: git-annex-remote-torrent

Here's a simple shell script example, which can easily be adapted to run whatever commands you need. Or better, re-written in some better language of your choice.

#!/bin/sh
# git-annex external special remote program
# 
# This is basically the same as git-annex's built-in directory special remote.
# 
# Install in PATH as git-annex-remote-directory
#
# Copyright 2013 Joey Hess; licenced under the GNU GPL version 3 or higher.

set -e

# This program speaks a line-based protocol on stdin and stdout.
# When running any commands, their stdout should be redirected to stderr
# (or /dev/null) to avoid messing up the protocol.
runcmd () {
	"$@" >&2
}

# Gets a value from the remote's configuration, and stores it in RET
getconfig () {
	ask GETCONFIG "$1"
}

# Stores a value in the remote's configuration.
setconfig () {
	echo SETCONFIG "$1" "$2"
}

# Sets LOC to the location to use to store a key.
calclocation () {
	ask DIRHASH "$1"
	LOC="$mydirectory/$RET/$1"
}

# Asks for some value, and stores it in RET
ask () {
	echo "$1" "$2"
	read resp
	# Tricky POSIX shell code to split first word of the resp,
	# preserving all other whitespace
	case "${resp%% *}" in
		VALUE)
			RET="$(echo "$resp" | sed 's/^VALUE \?//')"
		;;
		*)
			RET=""
		;;
	esac
}

# This remote doesn't need credentials to access it,
# but many of them will. Here's how to handle requiring the user
# set MYPASSWORD and MYLOGIN when running initremote. The creds
# will be stored securely for later use, so the user only needs
# to provide them once.
setupcreds () {
	if [ -z "$MYPASSWORD" ] || [ -z "$MYLOGIN" ]; then
		echo INITREMOTE-FAILURE "You need to set MYPASSWORD and MYLOGIN environment variables when running initremote."
	else
		echo SETCREDS mycreds "$MYLOGIN" "$MYPASSWORD"	
		echo INITREMOTE-SUCCESS
	fi
}

getcreds () {
	echo GETCREDS mycreds
	read resp
	case "${resp%% *}" in
		CREDS)
			MYLOGIN="$(echo "$resp" | sed 's/^CREDS \([^ ]*\) .*/\1/')"
			MYPASSWORD="$(echo "$resp" | sed 's/^CREDS [^ ]* //')"
		;;
	esac

}

# This has to come first, to get the protocol started.
echo VERSION 1

while read line; do
	set -- $line
	case "$1" in
		INITREMOTE)
			# Do anything necessary to create resources
			# used by the remote. Try to be idempotent.
			# 
			# Use GETCONFIG to get any needed configuration
			# settings, and SETCONFIG to set any persistent
			# configuration settings.
			# 
			# (Note that this is not run every time, only when
			# git annex initremote or git annex enableremote is
			# run.)

			# The directory provided by the user
			# could be relative; make it absolute,
			# and store that.
			getconfig directory
			mydirectory="$(readlink -f "$RET")" || true
			setconfig directory "$mydirectory"
			if [ -z "$mydirectory" ]; then
				echo INITREMOTE-FAILURE "You need to set directory="
			else
				if mkdir -p "$mydirectory"; then
					setupcreds
				else
					echo INITREMOTE-FAILURE "Failed to write to $mydirectory"
				fi
			fi
		;;
		PREPARE)
			# Use GETCONFIG to get configuration settings,
			# and do anything needed to get ready for using the
			# special remote here.
			getcreds
			getconfig directory
			mydirectory="$RET"
			if [ -d "$mydirectory" ]; then
				echo PREPARE-SUCCESS
			else
				echo PREPARE-FAILURE "$mydirectory not found"
			fi
		;;
		TRANSFER)
			key="$3"
			file="$4"
			case "$2" in
				STORE)
					# Store the file to a location
					# based on the key.
					# XXX when at all possible, send PROGRESS
					calclocation "$key"
					mkdir -p "$(dirname "$LOC")"
					# Store in temp file first, so that
					# CHECKPRESENT does not see it
					# until it is all stored.
					mkdir -p "$mydirectory/tmp"
					tmp="$mydirectory/tmp/$key"
					if runcmd cp "$file" "$tmp" \
					   && runcmd mv -f "$tmp" "$LOC"; then
						echo TRANSFER-SUCCESS STORE "$key"
					else
						echo TRANSFER-FAILURE STORE "$key"
					fi

					mkdir -p "$(dirname "$LOC")"
					# The file may already exist, so
					# make sure we can overwrite it.
					chmod 644 "$LOC" 2>/dev/null || true
				;;
				RETRIEVE)
					# Retrieve from a location based on
					# the key, outputting to the file.
					# XXX when easy to do, send PROGRESS
					calclocation "$key"
					if runcmd cp "$LOC" "$file"; then
						echo TRANSFER-SUCCESS RETRIEVE "$key"
					else
						echo TRANSFER-FAILURE RETRIEVE "$key"
					fi
				;;
			esac
		;;
		CHECKPRESENT)
			key="$2"
			calclocation "$key"
			if [ -e "$LOC" ]; then
				echo CHECKPRESENT-SUCCESS "$key"
			else
				if [ -d "$mydirectory" ]; then
					echo CHECKPRESENT-FAILURE "$key"
				else
					# When the directory does not exist,
					# the remote is not available.
					# (A network remote would similarly
					# fail with CHECKPRESENT-UNKNOWN
					# if it couldn't be contacted).
					echo CHECKPRESENT-UNKNOWN "$key" "this remote is not currently available"
				fi
			fi
		;;
		REMOVE)
			key="$2"
			calclocation "$key"
			# Note that it's not a failure to remove a
			# key that is not present.
			if [ -e "$LOC" ]; then
				if runcmd rm -f "$LOC"; then
					echo REMOVE-SUCCESS "$key"
				else
					echo REMOVE-FAILURE "$key"
				fi
			else
				echo REMOVE-SUCCESS "$key"
			fi
		;;
		*)
			# The requests listed above are all the ones
			# that are required to be supported, so it's fine
			# to say that any other request is unsupported.
			echo UNSUPPORTED-REQUEST
		;;
	esac	
done

# XXX anything that needs to be done at shutdown can be done here
Posted Thu Dec 26 22:14:52 2013