#!/bin/bash
# emacs: -*- mode: python; tab-width: 4; indent-tabs-mode: t -*-
# ex: set sts=4 ts=4 sw=4 noet:
#
# git-annex-remote-rclone - wrapper to enable use of rclone-supported cloud providers as git-annex special remotes.
#
# Install in PATH as git-annex-remote-rclone
#
# Copyright (C) 2016-2022  Daniel Dent
#               2022       git-annex-remote-rclone contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of version 3 of the GNU
# General Public License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
#
# Based on work originally copyright 2013 Joey Hess which was 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"
}

validate_layout() {
	if [ -z "$RCLONE_LAYOUT" ]; then
		RCLONE_LAYOUT="lower"
	fi
	case "$RCLONE_LAYOUT" in
		lower|directory|nodir|mixed|frankencase)
			;;
		*)
			echo "INITREMOTE-FAILURE rclone_layout setting not recognized"
			exit 1
			;;
	esac
}

# Sets LOC to the location to use to store a key.
calclocation () {
	case "$RCLONE_LAYOUT" in
		lower)
			ask DIRHASH-LOWER "$1"
			LOC="$REMOTE_TARGET:$REMOTE_PREFIX/$RET"
			;;
		directory)
			ask DIRHASH-LOWER "$1"
			LOC="$REMOTE_TARGET:$REMOTE_PREFIX/$RET$1/"
			;;
		nodir)
			LOC="$REMOTE_TARGET:$REMOTE_PREFIX/"
			;;
		mixed)
			ask DIRHASH "$1"
			LOC="$REMOTE_TARGET:$REMOTE_PREFIX/$RET"
			;;
		frankencase)
			ask DIRHASH "$1"
			lret=$(echo "$RET" | tr '[:upper:]' '[:lower:]')
			LOC="$REMOTE_TARGET:$REMOTE_PREFIX/$lret"
			;;
	esac
}

# Asks for some value, and stores it in RET
ask () {
	echo "$1" "$2"
	read -r resp
	# Strip trailing carriage return, if present
	resp="${resp%$'\r'}"
	if echo "$resp" | grep '^VALUE '>/dev/null; then
		RET=$(echo "$resp" | cut -f2- -d' ')
		else
		RET=""
	fi
}

printcmdresult() {
	cmd="$1"
	rc="$2"
	out="$3"
	# Replace explicit newline since we must provide 1 line DEBUG
	# Fancy sed is from Example 5 of https://linuxhint.com/newline_replace_sed
	# which worked on Linux and OSX.
	# shellcheck disable=SC2016
	out_safe=$(echo "$out" | sed -ne 'H;${x;s/\n/\\n/g;s/^,//;p;}')
	echo "DEBUG '$cmd' exited with rc=$rc and stdout=${out_safe}"
}

GREP () {
	set +e
	out=$(grep "$@")
	rc=$?
	set -e
	printcmdresult "grep \"$*\"" "$rc" "$out"
	return $rc
}

do_checkpresent() {
	key="$1"
	dest="$2"

	res=0
	check_result=$(rclone size --json "$dest" 2>/dev/null) || res=$?
	printcmdresult "rclone size --json \"$dest\"" "$res" "$check_result"
	count=$(echo "$check_result" | sed -En 's/^.*"count":\s*([0-9]+).*$/\1/ p')
	if [[ $res -eq 0 && "$count" -ge 1 ]]; then
		# Any nonzero object count means present.
		# Some rclone backends support multiple
		# files under one name.
		echo CHECKPRESENT-SUCCESS "$key"
	elif [[ $res -eq 0 && -n "$count" && "$count" -eq 0 ]]; then
		# A 0 object count means an empty directory.
		echo CHECKPRESENT-FAILURE "$key"
	elif [[ $res -eq 3 || $res -eq 4 ]]; then
		# file or directory doesn't exist
		# see https://rclone.org/docs/#exit-code
		echo CHECKPRESENT-FAILURE "$key"
	else
		echo CHECKPRESENT-UNKNOWN "$key" "remote currently unavailable or git-annex-remote-rclone failed to parse rclone output"
	fi
}

do_remove() {
	key="$1"
	dest="$2"

	# Note that it's not a failure to remove a
	# key that is not present.
	if remove_result=$(rclone delete --retries 1 "$dest" 2>&1); then
		echo REMOVE-SUCCESS "$key"
	else
		if echo "$remove_result" | GREP ' directory not found'; then
			echo REMOVE-SUCCESS "$key"
		else
			echo REMOVE-FAILURE "$key"
		fi
	fi
}

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

while read -r line; do
	# Strip trailing carriage return, if present
	line="${line%$'\r'}"
	# shellcheck disable=SC2086
	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.)

			getconfig prefix
			REMOTE_PREFIX=$RET
			if [ -z "$REMOTE_PREFIX" ]; then
				REMOTE_PREFIX="git-annex"
			fi
			if [ "$REMOTE_PREFIX" == "/" ]; then
				echo INITREMOTE-FAILURE "storing objects directly in the root (/) is not supported"
			fi
			setconfig prefix $REMOTE_PREFIX

			getconfig target
			REMOTE_TARGET=$RET
			setconfig target "$REMOTE_TARGET"

			getconfig rclone_layout
			RCLONE_LAYOUT=$RET
			validate_layout
			setconfig rclone_layout "$RCLONE_LAYOUT"

			if [ -z "$REMOTE_TARGET" ]; then
				echo INITREMOTE-FAILURE "rclone remote target must be specified (use target= parameter)"
			fi

			if runcmd rclone mkdir "$REMOTE_TARGET:$REMOTE_PREFIX"; then
				echo INITREMOTE-SUCCESS
			else
				echo INITREMOTE-FAILURE "Failed to create directory on remote. Ensure that 'rclone config' has been run."
			fi
		;;
		PREPARE)
			# Use GETCONFIG to get configuration settings,
			# and do anything needed to get ready for using the
			# special remote here.

			getconfig prefix
			REMOTE_PREFIX="$RET"

			getconfig target
			REMOTE_TARGET="$RET"

			getconfig rclone_layout
			RCLONE_LAYOUT="$RET"
			validate_layout

			echo PREPARE-SUCCESS
		;;
		TRANSFER)
			op="$2"
			key="$3"
			shift 3
			file="$*"
			case "$op" in
				STORE)
					# Store the file to a location
					# based on the key.
					# XXX when at all possible, send PROGRESS
					calclocation "$key"
					if [ ! -e "$file" ]; then
						echo TRANSFER-FAILURE STORE "$key" "asked to store non-existent file $file"
					else
						if runcmd rclone copy "$file" "$LOC"; then
							echo TRANSFER-SUCCESS STORE "$key"
						else
							echo TRANSFER-FAILURE STORE "$key"
						fi
					fi
				;;
				RETRIEVE)
					# Retrieve from a location based on
					# the key, outputting to the file.
					# XXX when easy to do, send PROGRESS
					calclocation "$key"
					# http://stackoverflow.com/questions/31396985/why-is-mktemp-on-os-x-broken-with-a-command-that-worked-on-linux
					if GA_RC_TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/rclone-annex-tmp.XXXXXXXXX") &&
						runcmd rclone copy "$LOC$key" "$GA_RC_TEMP_DIR" &&
						mv "$GA_RC_TEMP_DIR/$key" "$file" &&
						rmdir "$GA_RC_TEMP_DIR"; then
						echo TRANSFER-SUCCESS RETRIEVE "$key"
					else
						echo TRANSFER-FAILURE RETRIEVE "$key"
					fi
				;;
			esac
		;;
		CHECKPRESENT)
			key="$2"
			calclocation "$key"

			do_checkpresent "$key" "$LOC$key"
		;;
		REMOVE)
			key="$2"
			calclocation "$key"

			do_remove "$key" "$LOC$key"
		;;
		*)
			# 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
