#!/usr/local/bin/cbsd
#v12.1.5
MYARG="jname mode part"
MYOPTARG="compress ebytes emulator filestats hdrver header_extra jexport_exclude out threads"
MYDESC="Pack or extract chunk from from image"
ADDHELP="
  compress        - XZ compress level ( 0 - 9 ). Default is: 6. 0 mean is compression disabled
  ebytes          - expected bytes (for pass into cbsdtee) in extract mode
  filestats       - save bytes stats for part=data into file in pack mode
  header_extra    - can be as header_extra=\"test4=4,param=val\" or path to file. extra header data
  jexport_exclude - skip/exclude path in jail to export
  mode            - pack,extract
  part            - variants: header,rcconf,fstab,pkginfo,descr,info,sysdata,data
  out             - path_to_file for ascii-part or directory to extract for binary
                    or for binary image from pack mode
  threads         - xz-specific: the number of worker threads to use, default - 0 (inherits CPU cores num);
"

. ${subrdir}/nc.subr

filestats=
ebytes=
header_extra=
jexport_exclude=
threads=0
. ${cbsdinit}

extractchunk()
{
	local _dir

	if [ -n "${out}" ]; then
		_dir=$( ${DIRNAME_CMD} ${out} )
		if [ ! -d "${_dir}" ]; then
			${MKDIR_CMD} ${_dir}
			${CHOWN_CMD} ${cbsduser}:${cbsduser} ${_dir}
		fi
	fi

	[ -n "${out}" ] && ${TOUCH_CMD} ${out}

	case ${BINARY} in
		0)
			[ -z "${out}" ] && out="/dev/stdout"
			imghelper --start ${FROM} --end ${TO} --infile ${SRC} --outfile ${out}
			[ $? -ne 0 ] && err 1 "${N1_COLOR}imghelper error${N0_COLOR}"
			echo >> ${out}
			;;
		1)
			[ -z "${out}" ] && out=$( ${PWD_CMD} )
			# notes: multi-threaded decompress not implemented yet in BSD xz, so XZ_OPT does not matter yet
			if [ -z "${ebytes}" ]; then
				imghelper --start ${FROM} --end ${TO} --infile ${SRC} | env XZ_OPT="--threads=${_hwnum}" ${TAR_CMD} xpf - -C "${out}" --numeric-owner
			else
				imghelper --start ${FROM} --end ${TO} --infile ${SRC} | ${miscdir}/cbsdtee -e ${ebytes} | env XZ_OPT="--threads=${_hwnum}" ${TAR_CMD} xpf - -C "${out}" --numeric-owner
			fi
			[ $? -ne 0 ] && err 1 "${N1_COLOR}imghelper error${N0_COLOR}"
			;;
	esac

	return 0

}

# $hdrver used for determine correct struct of header
extractme()
{
	local _imgpart=0

	if [ -f "${jname}" ]; then
		SRC="${jname}"
	else
		SRC="${importdir}/${jname}.img"
		[ ! -f ${SRC} ] && err 1 "${N1_COLOR}No such image: ${N2_COLOR}${SRC}${N0_COLOR}"
	fi

	[ -n "${hdrver}" ] && _imgpart=$1

	DEST="${DATA}/${jname}"

	BINARY=0
	TO=0

	case "${part}" in
		"header")
			FROM="___NCSTART_HEADER=1"
			TO="___NCSTOP_HEADER=1"
			;;
		"rcconf")
			FROM="___NCSTART_RCCONF=1"
			TO="___NCSTOP_RCCONF=1"
			;;
		"fstab")
			FROM="___NCSTART_FSTAB=1"
			TO="___NCSTOP_FSTAB=1"
			;;
		"pkginfo")
			FROM="___NCSTART_PKGINFO=1"
			TO="___NCSTART_PKGINFO=1"
			;;
		"descr")
			FROM="___NCSTART_DESCR=1"
			TO="___NCSTOP_DESCR=1"
			;;
		"info")
			FROM="___NCSTART_INFO=1"
			TO="___NCSTOP_INFO=1"
			;;
		"localfstab")
			FROM="___NCSTART_LOCALFSTAB=1"
			TO="___NCSTOP_LOCALFSTAB=1"
			;;
		"sysdata")
			FROM="___NCSTART_SYSDATA=1"
			TO="___NCSTOP_SYSDATA=1"
			BINARY=1
			;;
		"data")
			FROM="___NCSTART_DATA=1"
			TO="___NCSTOP_DATA=1"
			BINARY=1
			;;
	esac

	extractchunk || err 1 "${N1_COLOR}Error: image has no FROM/TO symbols for ${part}. Probably wrong format version: ${N2_COLOR}${jname}${N0_COLOR}"
}

packme()
{
	local _hwnum=0
	local _dsk_list _zvol_list _zvol_name _my_nodename
	local real_jailsysdir real_datadir _i _header_extra _pos
	local _arg_len= _pref= ARG= VAL=

	if [ "${threads}" = "0" ]; then
		case "${platform}" in
			Linux)
				_hwnum=$(  ${GREP_CMD} -E "^vendor_id" /proc/cpuinfo | ${WC_CMD} -l )
				;;
			*)
				_hwnum=$( ${SYSCTL_CMD} -qn hw.ncpu )
				;;
		esac
	else
		_hwnum="${threads}"
	fi
	[ -z "${_hwnum}" ] && _hwnum="1"

	case ${part} in
		header)
			# use jailrcconfdir for back compatible with CBSD < 10.1.0
			[ ! -f "${jailrcconfdir}/rc.conf_${jname}" -a ! -f "${jailsysdir}/${jname}/rc.conf_${jname}" ] && err 1 "${N1_COLOR}No such rcconf for: ${N2_COLOR}$jname${N0_COLOR}"
			[ -f "${jailrcconfdir}/rc.conf_${jname}" ] && . ${jailrcconfdir}/rc.conf_${jname}
			[ -f "$jailsysdir/$jname/rc.conf_${jname}" ] && . ${jailsysdir}/${jname}/rc.conf_${jname}
			echo "add header"
			DT=$( ${DATE_CMD} "+%Y%m%d" )

			if [ -r ~cbsd/nodename ]; then
				_my_nodename=$( ${CAT_CMD} ~cbsd/nodename | ${AWK_CMD} '{printf $1}' )
			else
				_my_nodename=$( ${HOSTNAME_CMD} )
			fi

			${CAT_CMD} > ${out} <<EOF
___NCSTART_HEADER=1
hdrver="4"
jname="${jname}"
compress="${compress}"
arch="${arch}"
ver="${ver}"
date="${DT}"
emulator="${emulator}"
ip4_addr="${ip4_addr}"
host_hostname="${host_hostname}"
from_nodename="${_my_nodename}"
img_flat_size= ###########################################################################################
EOF

			# extra header case
			if [ -n "${header_extra}" ]; then
				strpos --str="${header_extra}" --search="="
				_pos=$?
				if [ ${_pos} -eq 0 ]; then
					# its file?
					if [ ! -r ${header_extra} ]; then
						err 1 "${N1_COLOR}${CBSD_APP}: header_extra not file and not param=value form: ${N2_COLOR}${header_extra}${N0_COLOR}"
					else
						# append extra header
						${GREP_CMD} . ${header_extra} >> ${out}
					fi
				else
					# find 'param=value' and append to header
					OIFS="${IFS}"
					IFS=","
					for _i in ${header_extra}; do
						IFS="${OIFS}"
						strpos --str="${_i}" --search="="
						_pos=$?
						if [ ${_pos} -eq 0 ]; then
							# not params=value form
							err 1 "${N1_COLOR}${CBSD_APP} error: header_extra not file and not param=value form: ${N2_COLOR}${header_extra}${N0_COLOR}"
						fi
						_arg_len=$( strlen ${_i} )
						_pref=$(( _arg_len - _pos ))
						ARG=$( substr --pos=0 --len=${_pos} --str="${_i}" )
						VAL=$( substr --pos=$(( ${_pos} +2 )) --len=${_pref} --str="${_i}" )
						printf "${ARG}='${VAL}'\n" >> ${out}
						IFS=","
					done
				fi
			fi

			# collect storage info
			case "${emulator}" in
				bhyve)
					# get info about storage

					_zvol_list=0

					# additional loop to check for symlink and zvol
					if [ ${zfsfeat} -eq 1 ]; then
						. ${subrdir}/zfs.subr
						_zvol_name_list=
						for i in $( ${FIND_CMD} ${data}/ -mindepth 1 -maxdepth 1 -type l ); do
							if is_getzvol ${i}; then
								_zvol_name=$( ${BASENAME_CMD} ${i} | ${SED_CMD} 's:\.vhd::g' )
								if [ -n "${_zvol_name_list}" ]; then
									_zvol_name_list="${_zvol_name_list} ${_zvol_name}"
								else
									_zvol_name_list="${_zvol_name}"
								fi
								imgbytes=$( ${ZFS_CMD} get -Hp -o value volsize ${is_zvol} )
								echo "vm_zvol_size_${_zvol_name}=\"${imgbytes}\"" >> ${out}
								_zvol_list=$(( _zvol_list + 1 ))
							fi
						done
					fi
					;;
				*)
			esac

			_dsk_list=$( cbsdsqlro ${jailsysdir}/${jname}/local.sqlite SELECT dsk_path FROM bhyvedsk WHERE jname=\"${jname}\" | ${SED_CMD} 's:\.vhd::g' | ${XARGS_CMD} )
			echo "vm_dsk_list=\"${_dsk_list}\"" >> ${out}
			echo "vm_zvol_list=\"${_zvol_name_list}\"" >> ${out}
			echo "___NCSTOP_HEADER=1" >> ${out}
			;;
		rcconf)
			# use jailrcconfdir for back compatible with CBSD < 10.1.0
			echo "___NCSTART_RCCONF=1" >> ${out}
			if [ -f "${jailsysdir}/${jname}/rc.conf_${jname}" ]; then
				${CAT_CMD} $jailsysdir/$jname/rc.conf_${jname} >> ${out}
			else
				[ -f "${jailrcconfdir}/rc.conf_${jname}" ] && ${CAT_CMD} ${jailrcconfdir}/rc.conf_${jname} >> ${out}
			fi
			echo "___NCSTOP_RCCONF=1" >> ${out}
			;;
		pkginfo)
			echo "___NCSTART_PKGINFO=1" >> ${out}
			PINFO="${jailsysdir}/${jname}/pkg_info"
			if [ -f "${PINFO}" ]; then
				${CAT_CMD} ${PINFO} >> ${out}
			else
				echo "No pinfo" >> ${out}
			fi
			echo "___NCSTOP_PKGINFO=1" >> ${out}
			;;
		descr)
			echo "___NCSTART_DESCR=1" >> ${out}
			DESCR="${jailsysdir}/${jname}/descr"
			if [ -f "${DESCR}" ]; then
				${CAT_CMD} ${DESCR} >> ${out}
			else
				echo "No descr" >> ${out}
			fi
			echo "___NCSTOP_DESCR=1" >> ${out}
			;;
		info)
			echo "___NCSTART_INFO=1" >> ${out}
			INFO="${jailsysdir}/${jname}/info"
			if [ -f "${INFO}" ]; then
				${CAT_CMD} ${INFO} >> ${out}
			else
				echo "No info" >> ${out}
			fi
			echo "___NCSTOP_INFO=1" >> ${out}
			;;
		fstab)
			echo "___NCSTART_FSTAB=1" >> ${out}
			# $jailfstabdir/$jname/fstab ( CBSD 12.1.5+ ) preferable
			if [ -r "${jailfstabdir}/${jname}/fstab" ]; then
				${CAT_CMD} ${jailfstabdir}/${jname}/fstab >> ${out}
			elif [ -r "${jailfstabdir}/${jailfstabpref}${jname}" ]; then
				${CAT_CMD} ${jailfstabdir}/${jailfstabpref}${jname} >> ${out}
			fi
			echo "___NCSTOP_FSTAB=1" >> ${out}
			;;
		localfstab)
			echo "___NCSTART_LOCALFSTAB=1" >> ${out}
			# $jailfstabdir/$jname/fstab.local ( CBSD 12.1.5+ ) preferable
			if [ -r "${jailfstabdir}/${jname}/fstab.local" ]; then
				${CAT_CMD} ${jailfstabdir}/${jname}/fstab.local >> ${out}
			elif [ -r "${jailfstabdir}/${jailfstabpref}${jname}.local" ]; then
				${CAT_CMD} ${jailfstabdir}/${jailfstabpref}${jname}.local >> ${out}
			fi
			echo "___NCSTOP_LOCALFSTAB=1" >> ${out}
			;;
		sysdata)
			echo "add sysdata"
			echo "___NCSTART_SYSDATA=1" >> ${out}
			[ ! -d ${jailsysdir}/${jname} -o ! -h ${jailsysdir}/${jname} ] && ${MKDIR_CMD} -p ${jailsysdir}/${jname}
			real_jailsysdir_jname=$( ${REALPATH_CMD} ${jailsysdir}/${jname} )
			real_jailsysdir=$( ${DIRNAME_CMD} ${real_jailsysdir_jname} )
			real_dir=$( ${BASENAME_CMD} ${real_jailsysdir_jname} )
			cd ${real_jailsysdir}
			TMPCHUNK="$tmpdir/sys$$.tgz"
			trap "${RM_CMD} -f ${TMPCHUNK}" 0 1 2 3 4
			# exclude ^dsk[0-9]+ files when dir consolidated (e.g: zfs)
			${TAR_CMD} cfz ${TMPCHUNK} --exclude "${real_dir}/dsk[0-9]*.vhd" --numeric-owner ${real_dir} > /dev/null 2>&1
			${CAT_CMD} ${TMPCHUNK} >> ${out}
			echo >> ${out}
			echo "___NCSTOP_SYSDATA=1" >> ${out}
			;;
		data)
			echo "add data"
			# use jailrcconfdir for back compatible with CBSD < 10.1.0
			[ ! -f "${jailrcconfdir}/rc.conf_${jname}" -a ! -f "${jailsysdir}/${jname}/rc.conf_${jname}" ] && err 1 "${N1_COLOR}No such rcconf for: ${N2_COLOR}$jname${N0_COLOR}"
			[ -f ${jailrcconfdir}/rc.conf_${jname} ] && . ${jailrcconfdir}/rc.conf_${jname}
			[ -f ${jailsysdir}/${jname}/rc.conf_${jname} ] && . ${jailsysdir}/${jname}/rc.conf_${jname}
			#eval data=\"\$data\"
			real_datadir_jname=$( ${REALPATH_CMD} ${data} )			# /usr/jails/jails-data/XXX-data
			real_datadir=$( ${DIRNAME_CMD} ${real_datadir_jname} )		# /usr/jails/jails-data
			real_dir=$( ${BASENAME_CMD} ${real_datadir_jname} )		# XXX-data
			[ -z "${real_datadata}" -a ! -d "${real_datadir}" ] && err 1 "No such data directory ${real_datadir}."
			cd ${real_datadir}
			if [ ! -d ${real_dir} ]; then
				real_dir="${jname}-data"
				${MKDIR_CMD} -p ${real_dir}
			fi

			echo "___NCSTART_DATA=1" >> ${out}
			[ -z "${ebytes}" ] && ebytes=0

			# create exclude file list based on md5 when jailsysdir in symlink and consolidated with data dir (e.g: ZFS feat)
			if [ -h ${jailsysdir}/${jname} -a -h ${jaildatadir}/${jname}-${jaildatapref} ]; then
				_res1=$( ${REALPATH_CMD} ${jailsysdir}/${jname} )
				_res2=$( ${REALPATH_CMD} ${jaildatadir}/${jname}-${jaildatapref} )
				if [ "${_res1}" = "${_res2}" ]; then
					${FIND_CMD} ${_res1}/ -mindepth 1 -maxdepth 1 \( -type l -or -type f -or -type d \) -and -not -regex "${_res1}/dsk[0-9]*\.vhd" -exec ${BASENAME_CMD} {} \; > ${tmpdir}/exclude.txt.$$
					_sys_exclude="-X ${tmpdir}/exclude.txt.$$"
				else
					_sys_exclude=
				fi
			fi

			if [ -n "${jexport_exclude}" ]; then
				for i in ${jexport_exclude}; do
					echo "${real_dir}${i}" >> ${tmpdir}/exclude.txt.$$
				done
				_sys_exclude="-X ${tmpdir}/exclude.txt.$$"
			fi

			case "${compress}" in
				0)
					# no compression
					if [ -n "${filestats}" ]; then
						# save bytes statistics to ${filestats}
						printf " ${H3_COLOR}"
						${NICE_CMD} -n 20 ${TAR_CMD} cf - ${_sys_exclude} --numeric-owner --format gnutar ${real_dir} | ${miscdir}/cbsdtee -e ${ebytes} -f ${filestats} >> ${out}
						printf "${N0_COLOR}"
					else
						${NICE_CMD} -n 20 ${TAR_CMD} cf - ${_sys_exclude} --numeric-owner --format gnutar ${real_dir} >> ${out}
					fi
					;;
				*)
					# cbsdtee got buffered data?
					if [ -n "${filestats}" ]; then
						printf " ${H3_COLOR}"
						${NICE_CMD} -n 20 ${TAR_CMD} cf - ${_sys_exclude} --numeric-owner --format gnutar ${real_dir} | ${miscdir}/cbsdtee -e ${ebytes} -f ${filestats} | ${NICE_CMD} -n 20 ${XZ_CMD} -${compress}e -T${_hwnum} >> ${out}
						printf "${N0_COLOR}"
					else
						${NICE_CMD} -n 20 ${TAR_CMD} cf - ${_sys_exclude} --numeric-owner --format gnutar ${real_dir} | ${NICE_CMD} -n 20 ${XZ_CMD} -${compress}e -T${_hwnum} >> ${out}
					fi
					;;
			esac

			[ -r ${tmpdir}/exclude.txt.$$ ] && ${RM_CMD} -f ${tmpdir}/exclude.txt.$$

			echo >> ${out}
			echo "___NCSTOP_DATA=1" >> ${out}

			if [ -n "${filestats}" -a -r "${filestats}" ]; then
				. ${filestats}
				img_flat_size_summary="${img_flat_size}"
			else
				img_flat_size_summary=0
			fi

			# additional loop to check for symlink and zvol
			if [ ${zfsfeat} -eq 1 ]; then
				. ${subrdir}/zfs.subr
				for i in $( ${FIND_CMD} ${data}/ -mindepth 1 -maxdepth 1 -type l ); do
					if is_getzvol ${i}; then
						_zvol_name=$( ${BASENAME_CMD} ${i} | ${SED_CMD} 's:\.vhd::g' )
						echo "___NCSTART_ZVOL_${_zvol_name}=1" >> ${out}
						if [ -n "${filestats}" ]; then
							# Linux does not support postfix in bs=, e.g. bs=1m
							case "${platform}" in
								Linux)
									${DD_CMD} if=/dev/zvol/${is_zvol} bs=1000000 | ${miscdir}/cbsdtee -f ${filestats} | ${GZIP_CMD} --fast -c >> ${out}
									;;
								*)
									${DD_CMD} if=/dev/zvol/${is_zvol} bs=1m | ${miscdir}/cbsdtee -f ${filestats} | ${GZIP_CMD} --fast -c >> ${out}
									;;
							esac
							. ${filestats}
							img_flat_size_summary=$(( img_flat_size_summary + img_flat_size ))
						else
							# Linux does not support postfix in bs=, e.g. bs=1m
							case "${platform}" in
								Linux)
									${DD_CMD} if=/dev/zvol/${is_zvol} bs=1000000 | ${GZIP_CMD} --fast -c >> ${out}
									;;
								*)
									${DD_CMD} if=/dev/zvol/${is_zvol} bs=1000000 | ${GZIP_CMD} --fast -c >> ${out}
									;;
							esac
						fi
						echo >> ${out}
						echo "___NCSTOP_ZVOL_${_zvol_name}=1" >> ${out}
					fi
				done
			fi

			# update flat_size in image
			if [ -n "${filestats}" -a -r "${filestats}" ]; then
				. ${filestats}
				imghelper --start ___NCSTART_HEADER=1 --end ___NCSTOP_HEADER=1 --infile ${out} --param=img_flat_size --newval="${img_flat_size_summary}"
			fi
			;;
	esac
}

# MAIN
# cwd necessary for the relative actions
cd ${workdir}

[ -z "${compress}" ] && compress=6

case "${mode}" in
	extract)
		extractme || err 1 "Error extract"
		;;
	pack)
		. ${subrdir}/rcconf.subr
		[ $? -eq 1 ] && err 1 "${N1_COLOR}No such jail: ${N2_COLOR}${jname}${N0_COLOR}"

		# used by zfs-migrator online. Not neccesary to check in this place?
		#[ ${jid} -ne 0 -a "${emulator}" = "bhyve" ] && err 1 "${N1_COLOR}VM is online${N0_COLOR}"
		[ -z "${out}" ] && err 1 "${N1_COLOR}out=tofile is mandatory${N0_COLOR}"
		packme
		;;
esac

exit 0
