#!/usr/bin/env bash
# Librerelease
# Uploads packages and releases them

# Copyright (C) 2010-2012 Joshua Ismael Haase Hernández (xihh) <hahj87@gmail.com>
# Copyright (C) 2010-2013 Nicolás Reynolds <fauno@parabola.nu>
# Copyright (C) 2013 Michał Masłowski <mtjm@mtjm.eu>
# Copyright (C) 2013-2014, 2017-2018 Luke Shumaker <lukeshu@parabola.nu>
#
# For just the create_signature() function:
#   Copyright (C) 2006-2013 Pacman Development Team <pacman-dev@archlinux.org>
#   Copyright (C) 2002-2006 Judd Vinet <jvinet@zeroflux.org>
#   Copyright (C) 2005 Aurelien Foret <orelien@chez.com>
#   Copyright (C) 2006 Miklos Vajna <vmiklos@frugalware.org>
#   Copyright (C) 2005 Christian Hamar <krics@linuxforum.hu>
#   Copyright (C) 2006 Alex Smith <alex@alex-smith.me.uk>
#   Copyright (C) 2006 Andras Voroskoi <voroskoi@frugalware.org>
#
# License: GNU GPLv3+
#
# This file is part of Parabola.
#
# Parabola is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Parabola 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.
#
# You should have received a copy of the GNU General Public License
# along with Parabola. If not, see <http://www.gnu.org/licenses/>.

# create_signature() is taken from pacman:makepkg, which is GPLv2+,
# so we take the '+' to combine it with our GPLv3+.

set -euE
. "$(librelib messages)"
. "$(librelib conf)"
setup_traps

dryrun=""
upload_only=false
readonly rsync_flags=(
	--no-group
	--no-perms
	--copy-links
	--hard-links
	--partial
	--human-readable
	--progress
)

# Functions ####################################################################

list0_files() {
	find -L "${WORKDIR}/staging" -type f -not -name '*.lock' \
		-exec realpath -z --relative-to="${WORKDIR}/staging" {} +
}

# This function is taken almost verbatim from makepkg
create_signature() {
	local ret=$EXIT_SUCCESS
	local filename="$1"
	msg "Signing package..."

	local SIGNWITHKEY=()
	if [[ -n $GPGKEY ]]; then
		SIGNWITHKEY=(-u "${GPGKEY}")
	fi

	gpg --detach-sign --use-agent "${SIGNWITHKEY[@]}" --no-armor "$filename" &>/dev/null || ret=$EXIT_FAILURE


	if (( ! ret )); then
		msg2 "Created signature file %s." "$filename.sig"
	else
		error "Failed to sign package file."
		return $ret
	fi
}

sign_packages() {
	IFS=$'\n'
	local files=($(find "${WORKDIR}/staging/" -type f -not -iname '*.sig' -print))
	local file
	for file in "${files[@]}"; do
		if [[ -f "${file}.sig" ]]; then
			msg2 "File signature found, verifying..."

			# Verify that the signature is correct, else remove for re-signing
			if ! gpg --quiet --verify "${file}.sig" >/dev/null 2>&1; then
				error "Failed!  Re-signing..."
				rm -f "${file}.sig"
			fi
		fi

		if ! [[ -f "${file}.sig" ]]; then
			create_signature "$file" || return
		fi
	done
}

# Clean everything if not in dry-run mode
clean_files() (
	local file_list=$1

	local rmcmd=(rm -fv)
	if [[ -n "${dryrun}" ]]; then
		rmcmd=(printf "$(_ "removed '%s' (dry-run)")\n")
	fi

	msg "Removing files from local staging directory"
	cd "${WORKDIR}/staging"
	xargs -0r -a "$file_list" "${rmcmd[@]}"
	find . -depth -mindepth 1 -type d \
	     -exec rmdir --ignore-fail-on-non-empty -- '{}' +
)

################################################################################

usage() {
	print "Usage: %s [OPTIONS]" "${0##*/}"
	print 'Upload packages in $WORKDIR/staging to the Parabola server'
	echo
	print "Options:"
	flag '-c'            'Clean; delete packages in $WORKDIR/staging'
	flag '-l'            "List; list packages but not upload them"
	flag '-u'            "Upload-only; do not run db-update on the server"

	flag '-n'            "Dry-run; don't actually do anything"
	flag '-h'            "Show this message"
}

main() {
	if [[ -w / ]]; then
		error "This program should be run as regular user"
		return $EXIT_NOPERMISSION
	fi

	# Parse options
	local mode="release_packages"
	while getopts 'clunh' arg; do
		case $arg in
			c) mode=clean ;;
			l) mode=pretty_print_packages ;;
			u) upload_only=true ;;
			n) dryrun="--dry-run" ;;
			h) mode=usage ;;
			*) usage >&2; return $EXIT_INVALIDARGUMENT ;;
		esac
	done
	shift $((OPTIND - 1))
	if [[ $# != 0 ]]; then
		usage >&2
		return $EXIT_INVALIDARGUMENT
	fi

	if [[ $mode == usage ]]; then
		usage
		return $EXIT_SUCCESS
	fi

	declare -i ret=0
	load_conf makepkg.conf GPGKEY || ret=$?
	load_conf libretools.conf WORKDIR REPODEST DBSCRIPTS_CONFIG || ret=$? # and HOOK{PRE,POST}RELEASE, which are optional
	[[ $ret = 0 ]] || exit $ret

	local re_url='^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$'
	local re_authority='^(([^@]*)@)?([^][@:]*|\[[^]]*\])(:([0-9]*))?$'
	local REPODEST_ok=false
	if [[ "$REPODEST" =~ $re_url ]]; then
		REPODEST_ok=true

		REPODEST_scheme=${BASH_REMATCH[2]}
		REPODEST_authority=${BASH_REMATCH[4]}
		REPODEST_path=${BASH_REMATCH[5]}
		REPODEST_query=${BASH_REMATCH[7]}
		REPODEST_fragment=${BASH_REMATCH[9]}

		if [[ "$REPODEST_authority" =~ $re_authority ]]; then
			REPODEST_userinfo=${BASH_REMATCH[2]}
			REPODEST_host=${BASH_REMATCH[3]}
			REPODEST_port=${BASH_REMATCH[5]}

			if [[ "$REPODEST_host" = '['*']' ]]; then
				REPODEST_host=${REPODEST_HOST#'['}
				REPODEST_host=${REPODEST_HOST#']'}
			fi
		else
			REPODEST_ok=false
		fi

		[[ $REPODEST_scheme == ssh ]] || REPODEST_ok=false
		[[ -n $REPODEST_host ]] || REPODEST_ok=false
		[[ -n $REPODEST_path ]] || REPODEST_ok=false
	fi
	if ! $REPODEST_ok; then
		error 'The format of libretools.conf:REPODEST has changed.'
		plain 'Merge the /etc/libretools.conf.pacnew file!'
		return $EXIT_NOTCONFIGURED
	fi
	if [[ "$REPODEST_path" = '/~'* ]]; then
		if [[ "$REPODEST_path" = '/~/'* ]]; then
			REPODEST_path=${REPODEST_path#'/~/'}
		else
			error 'Unfortunately, `~user` home-directory expansion is not (yet?) supported in libretools.conf:REPODEST'
			return $EXIT_NOTCONFIGURED
		fi
	fi
	REPODEST_userhost="${REPODEST_userinfo:+${REPODEST_userinfo%%:*}@}${REPODEST_host}"

	"$mode"
}

# The different modes (sans 'usage') ###########################################

pretty_print_packages() {
	find "$WORKDIR/staging/" -mindepth 1 -maxdepth 1 -type d -not -empty | sort |
	while read -r path; do
		msg2 "${path##*/}"
		cd "$path"
		find -L . -type f -not -name '*.lock' | sed 's|^\./|     |' | sort
	done
}

clean() {
	lock 8 "${WORKDIR}/staging.lock" \
		'Waiting for an exclusive lock on the staging directory'

	local file_list
	file_list="$(mktemp -t "${0##*/}.XXXXXXXXXX")"
	trap "rm -f -- ${file_list@Q}" EXIT
	list0_files > "$file_list"

	lock_close 8

	clean_files "$file_list"
}

release_packages() {
	if [[ -n $HOOKPRERELEASE ]]; then
		msg "Running HOOKPRERELEASE..."
		(
			PS4="   \\[$BOLD\\]\$\\[$ALL_OFF\\] "
			eval -- "set -x; $HOOKPRERELEASE"
		)
	fi

	lock 8 "${WORKDIR}/staging.lock" \
		'Waiting for an exclusive lock on the staging directory'

	sign_packages || return

	# Make the permissions of the packages 644 otherwise the user will get access
	# denied error when they try to download (rsync --no-perms doesn't seem to
	# work).
	find "${WORKDIR}/staging" -type f -exec chmod 644 {} +
	find "${WORKDIR}/staging" -type d -exec chmod 755 {} +

	local file_list="$(mktemp -t ${0##*/}.XXXXXXXXXX)"
	trap "rm -f -- ${file_list@Q}" EXIT
	list0_files > "$file_list"

	lock_close 8

	msg "%s to upload" "$(cd "${WORKDIR}/staging" && du -hc --files0-from="$file_list" | sed -n '$s/\t.*//p')"
	msg "Uploading packages..."
	xargs -0r -a "$file_list" dirname -z | ssh ${REPODEST_port:+-p "$REPODEST_port"} "${REPODEST_userhost}" "mkdir -p -- ${REPODEST_path@Q} && cd ${REPODEST_path@Q} && xargs -0r mkdir -pv --"
	if ! rsync ${dryrun} "${rsync_flags[@]}" \
		-e "ssh ${REPODEST_port:+-p $REPODEST_port}" \
		-0 --files-from="$file_list" \
		"${WORKDIR}/staging" \
		"$REPODEST_userhost:$REPODEST_path/"
	then
		error "Sync failed, try again"
		return $EXIT_FAILURE
	fi

	clean_files "$file_list"

	if $upload_only; then
		return $EXIT_SUCCESS
	fi

	msg "Running db-update on repos"
	ssh ${REPODEST_port:+-p "$REPODEST_port"} "${REPODEST_userhost}" "STAGING=${REPODEST_path@Q} DBSCRIPTS_CONFIG=${DBSCRIPTS_CONFIG@Q} db-update"

	if [[ -n $HOOKPOSTRELEASE ]]; then
		msg "Running HOOKPOSTRELEASE..."
		(
			PS4="   \\[$BOLD\\]\$\\[$ALL_OFF\\] "
			eval -- "set -x; $HOOKPOSTRELEASE"
		)
	fi

	return $EXIT_SUCCESS
}

main "$@"
