Skip to content

Instantly share code, notes, and snippets.

@mfeldheim
Created April 1, 2015 08:45
Show Gist options
  • Save mfeldheim/1722ae67165e1ecbbc19 to your computer and use it in GitHub Desktop.
Save mfeldheim/1722ae67165e1ecbbc19 to your computer and use it in GitHub Desktop.
screenshotting jobs
#!/bin/bash
function print_message()
{
ADD_STRING="${1}"
shift
while [ -n "${1}" ]
do
ADD_STRING="${ADD_STRING}${1}"
shift
done
printf "ID %d, Aufruf %d, Kunde %d, Eintrag %d: %s\n" \
"${DB_ID}" \
"${CALL__DEPTH}" \
"${CUSTOMER_ID}" \
"${EINTRAG__ID}" \
"${ADD_STRING}"
}
WEBSERVER_USER="wwwrun"
WEBSERVER_GROUP="www"
LOGPATH="/var/log"
SCRIPTNAME=$(basename $0 .sh)
MAX_NUM_CALLS_FOR_SLEEP_INCREASE=5
MAX_NUM_RECURSIVE_CALLS=10
THUMBNAIL_BREITE=100
SCREENSHOT_BREITE=202
# log file names
LOG_FILE="${SCRIPTNAME}Request.log"
ERRLOG_FILE="${SCRIPTNAME}RequestError.log"
PCNTL_LOG_FILE="${SCRIPTNAME}RequestPcntl.log"
# system settings
SERVER_PATH="/srv/www/vhosts/media/httpdocs"
SLEEP=
SLEEPTIME=30
SLEEPTIME_ADD=120
PARAMS=
CALL__DEPTH=0
ORIG_LINE=
# DB parameters
DB_NAME=
DB_USER=
DB_PASS=
DB_TABLE=
DB_CALL=
DB_ID=
CUSTOMER_ID="INVALID"
EINTRAG__ID="INVALID"
IMAGE__PATH="INVALID"
UTF8____URL="INVALID"
ENCODED_URL="INVALID"
LATIN___URL="INVALID"
while [ -n "${1}" ] ; do
case ${1} in
--database-name=* )
PARAMS="${PARAMS} ${1}"
DB_NAME=`echo ${1} | sed -e 's/--database-name=//g'`
shift
;;
--database-user=* )
PARAMS="${PARAMS} ${1}"
DB_USER=`echo ${1} | sed -e 's/--database-user=//g'`
shift
;;
--database-pass=* )
PARAMS="${PARAMS} ${1}"
DB_PASS=`echo ${1} | sed -e 's/--database-pass=//g'`
shift
;;
--database-table=* )
PARAMS="${PARAMS} ${1}"
DB_TABLE=`echo ${1} | sed -e 's/--database-table=//g'`
shift
;;
--database-id=* )
PARAMS="${PARAMS} ${1}"
DB_ID=`echo ${1} | sed -e 's/--database-id=//g'`
shift
;;
--log-path=* )
CHKPATH=`echo ${1} | sed -e 's/--log-path=//g'`
if [ ! -z "${CHKPATH}" ] ; then
if [ -e "${CHKPATH}" ] ; then
if [ ! -d "${CHKPATH}" ] ; then
echo "requested log path '${LOGPATH}' exists but is no directory"
exit 99
fi
else
mkdir -p "${CHKPATH}" > /dev/null 2>&1
if [ "$?" != "0" ] ; then
echo "failed to create log path '${LOGPATH}'"
exit 99
fi
fi
LOGPATH="${CHKPATH}"
fi
shift
;;
--log-file=* )
LOG_FILE=`echo ${1} | sed -e 's/--log-file=//g'`
shift
;;
--error-log-file=* )
ERRLOG_FILE=`echo ${1} | sed -e 's/--error-log-file=//g'`
shift
;;
--pcntl-log-file=* )
PCNTL_LOG_FILE=`echo ${1} | sed -e 's/--pcntl-log-file=//g'`
shift
;;
--server-path=* )
CHECK_PATH=`echo ${1} | sed -e 's/--server-path=//g'`
if [ -d "${CHECK_PATH}" ]
then
SERVER_PATH="${CHECK_PATH}"
fi
shift
;;
--sleep-time=* )
SLEEPTIME=`echo ${1} | sed -e 's/--sleep-time=//g'`
shift
;;
--sleep=* )
SLEEP=`echo ${1} | sed -e 's/--sleep=//g'`
shift
;;
--call-number=* )
CALL__DEPTH=`echo ${1} | sed -e 's/--call-number=//g'`
shift
;;
* )
shift
;;
esac
done
PARAMS="${PARAMS} --log-path=${LOGPATH} --log-file=${LOG_FILE} --pcntl-log-file=${PCNTL_LOG_FILE} --error-log-file=${ERRLOG_FILE} --server-path=${SERVER_PATH}"
LOG="${LOGPATH}/${LOG_FILE}"
touch "${LOG}"
if [ "$?" != "0" ] ; then
echo "could not find request log file '${LOG}'"
exit 99
fi
PCNTLLOG="${LOGPATH}/${PCNTL_LOG_FILE}"
touch "${PCNTLLOG}"
if [ "$?" != "0" ] ; then
echo "could not find request process control log file '${PCNTLLOG}'" >> ${LOG}
exit 99
fi
ERRLOG="${LOGPATH}/${ERRLOG_FILE}"
touch "${ERRLOG}"
if [ "$?" != "0" ] ; then
echo "could not find request error log file '${ERRLOG}'" >> ${LOG}
exit 99
fi
if [ -n "${DB_ID}" -a -n "${DB_NAME}" -a -n "${DB_USER}" -a -n "${DB_PASS}" -a -n "${DB_TABLE}" ] ; then
DB_CALL="mysql ${DB_NAME} --user=${DB_USER} --password=${DB_PASS} --skip-column-names --batch"
SQL_QUERY="USE ${DB_NAME}"
CHECK_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
echo -e "ERROR: ID '${DB_ID}': ${CHECK_RESULT}" >> ${ERRLOG}
exit 9
fi
SQL_QUERY="SELECT kunde_id, eintrag_id, imagePath, ownerPath, url, encodedUrl, latinUrl FROM ${DB_TABLE} WHERE id = '${DB_ID}'"
# SQL_QUERY="SELECT kunde_id, eintrag_id, imagePath, url, encodedUrl, latinUrl FROM ${DB_TABLE} WHERE id = '${DB_ID}'"
## # RAW_QUERY_RESULT=`${DB_CALL} --execute="${SQL_QUERY}"`
## # QUERY_RESULT=`echo "${RAW_QUERY_RESULT}" | grep -E "((-)?[1-9][0-9]*)[[:space:]]((-)?[1-9][0-9]*)[[:space:]]([^/]+/)+([0-9]{2}/){6}screenshots/[[:space:]]([^[:space:]]+)[[:space:]]([^[:space:]]+)[[:space:]]([^[:space:]]+)"`
QUERY_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" | grep -E "(-?[1-9][0-9]*)[[:space:]](-?[1-9][0-9]*)[[:space:]]([^/]+/)+([0-9]{2}/){6}screenshots/[[:space:]]([^/]+/)+([0-9]{2}/){5}[[:space:]]([^[:space:]]+)[[:space:]]([^[:space:]]+)[[:space:]]([^[:space:]]+)"`
# QUERY_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" | grep -E "(-?[1-9][0-9]*)[[:space:]](-?[1-9][0-9]*)[[:space:]]([^/]+/)+([0-9]{2}/){6}screenshots/[[:space:]]([^[:space:]]+)[[:space:]]([^[:space:]]+)[[:space:]]([^[:space:]]+)"`
if [ -n "${QUERY_RESULT}" ] ; then
declare -a PARSED_DATA
PARSED_DATA=( `echo "${QUERY_RESULT}"` )
if [ "${#PARSED_DATA[@]}" == "7" ] ; then
# if [ "${#PARSED_DATA[@]}" == "6" ] ; then
CUSTOMER_ID="${PARSED_DATA[0]}"
EINTRAG__ID="${PARSED_DATA[1]}"
IMAGE__PATH="${PARSED_DATA[2]}"
OWNER__PATH="${PARSED_DATA[3]}"
UTF8____URL="${PARSED_DATA[4]}"
ENCODED_URL="${PARSED_DATA[5]}"
LATIN___URL="${PARSED_DATA[6]}"
else
echo -e "ERROR: ID '${DB_ID}': failed to extraxt data from query result" >> ${ERRLOG}
exit 7
fi
else
echo -e "ERROR: ID '${DB_ID}': filtered query result from database empty" >> ${ERRLOG}
exit 8
fi
unset PARSED_DATA
else
printf "DB Parameter Error: incomplete parameters passed -->\n\t%s '%s'\n\t%s '%s'\n\t%s '%s'\n\t%s '%s'\n\t%s '%s'\n" \
"--database-id :" "${DB_ID}" \
"--database-name :" "${DB_NAME}" \
"--database-user :" "${DB_USER}" \
"--database-pass :" "${DB_PASS}" \
"--database-table :" "${DB_TABLE}" >> ${ERRLOG}
exit 1
fi
# printf "CUSTOMER_ID='%d'\nEINTRAG__ID='%d'\nIMAGE__PATH='%s'\nUTF8____URL='%s'\nENCODED_URL='%s'\nLATIN___URL='%s'\n" \
# "${CUSTOMER_ID}" \
# "${EINTRAG__ID}" \
# "${IMAGE__PATH}" \
# "${UTF8____URL}" \
# "${ENCODED_URL}" \
# "${LATIN___URL}" >> ${LOG}
# exit
if [ ${CALL__DEPTH} -gt ${MAX_NUM_CALLS_FOR_SLEEP_INCREASE} ] ; then
OLD_SLEEPTIME=${SLEEPTIME}
SLEEPTIME=`/usr/bin/expr ${SLEEPTIME} + ${SLEEPTIME_ADD}`
print_message "increasing sleep time from ${OLD_SLEEPTIME} to ${SLEEPTIME} seconds" >> ${PCNTLLOG}
fi
if [ ${CALL__DEPTH} -gt ${MAX_NUM_RECURSIVE_CALLS} ] ; then
print_message "max allowed number of recursive calls (" ${MAX_NUM_RECURSIVE_CALLS} ") exceeded" >> ${ERRLOG}
SQL_QUERY="UPDATE ${DB_TABLE} SET status = '8', callNumber = '${CALL__DEPTH}', requestStatus = '0', requestTime = NOW(), lastErrorMessage = 'max allowed number of recursive calls exceeded', recursionError = '${CALL__DEPTH}' WHERE id = '${DB_ID}'"
CHECK_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
print_message "database update error: ${CHECK_RESULT}, QUERY: ${SQL_QUERY}" >> ${ERRLOG}
exit 9
fi
exit 2
fi
CALL__DEPTH_NEXTCALL=`/usr/bin/expr ${CALL__DEPTH} + 1`
if [ -n "${SLEEP}" ] ; then
sleep "${SLEEPTIME}"
fi
# temporary file to work with
TMP_CALL_DEPTH_STRING=${CALL__DEPTH}
while [ ${#TMP_CALL_DEPTH_STRING} -lt 2 ]
do
TMP_CALL_DEPTH_STRING="0${TMP_CALL_DEPTH_STRING}"
done
TMPFILE=`mktemp -t "screenshot_${EINTRAG__ID}_${TMP_CALL_DEPTH_STRING}_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"`
if [ "${CALL__DEPTH}" == 0 ] ; then
print_message "requesting screenshot for url '${ENCODED_URL}'" >> ${LOG}
fi
# man curl:
#
# ...
# --connect-timeout <seconds>
# Maximum time in seconds that you allow the connection to the server to take.
# This only limits the connection phase, once curl has connected this option is of no more use.
# See also the -m/--max-time option.
#
# If this option is used several times, the last one will be used.
# ...
# -m/--max-time <seconds>
# Maximum time in seconds that you allow the whole operation to take.
# This is useful for preventing your batch jobs from hanging for hours due to slow networks or links going down.
# This doesn't work fully in win32 systems.
#
# See also the --connect-timeout option.
#
# If this option is used several times, the last one will be used.
# ...
# file downloaden
REQUEST_URL="http://stadtbranchenbuch.websnapr.com/?size=L&key=abet1V5Nbz2a&url=${ENCODED_URL}"
curl --connect-timeout 5 --max-time 30 -s -i -o "${TMPFILE}" "${REQUEST_URL}" >/dev/null 2>&1
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
rm -f "${TMPFILE}" > /dev/null 2>&1
# print_message "curl request failed, request url was '${REQUEST_URL}'"
print_message "curl request failed, request url was '${REQUEST_URL}'" >> ${ERRLOG}
SQL_QUERY="UPDATE ${DB_TABLE} SET status = '6', callNumber = '${CALL__DEPTH}', requestStatus = '0', requestTime = NOW(), lastErrorMessage = 'curl request failed', curlError = '${RETCODE}' WHERE id = '${DB_ID}'"
CHECK_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
print_message "database update error: ${CHECK_RESULT}, QUERY: ${SQL_QUERY}" >> ${ERRLOG}
exit 9
fi
exit 3
fi
# filesize bestimmen
FILESIZE=`cat "${TMPFILE}" | wc -c`
HEADERSIZE=0;
if [ "${FILESIZE}" == "0" ] ; then
print_message "0 byte file repeating call for url '${ENCODED_URL}'" >> ${ERRLOG}
rm -f "${TMPFILE}" > /dev/null 2>&1
# echo "$0 $PARAMS --call-number=${CALL__DEPTH_NEXTCALL} --sleep=1 &"
$0 $PARAMS --call-number=${CALL__DEPTH_NEXTCALL} --sleep=1 --sleep-time=${SLEEPTIME} &
exit
fi
################################################################
# file �ffnen ( mit weird umleitungen � la Ollis Helle stunden #
################################################################
exec 5<&0
exec <"${TMPFILE}"
BODYSTARTED="false";
# file zeilenweise durchgehen ( wird an \n getrennt )
while read LINE
do
# whitespaces aus der Zeile entfernen um den header / body trenner feststellen zu k�nnen
CLEANSTRING=`echo "${LINE}" | sed -e 's/\s//g'`
# header / body trenner feststellen
if [ "${BODYSTARTED}" == "false" -a -z "${CLEANSTRING}" ] ; then
BODYSTARTED="true"
# Pfad f�r die Ablage des Bildes anlegen ( -p f�r faule )
MAKEPATH="${SERVER_PATH}/${IMAGE__PATH}"
CHOWNPATH="${SERVER_PATH}/${OWNER__PATH}"
mkdir -p "${MAKEPATH}"
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
exec 0<&5 5<&-
rm -f "${TMPFILE}" > /dev/null 2>&1
print_message "mkdir error, return code '${RETCODE}', screenshot path '${MAKEPATH}'" >> ${ERRLOG}
SQL_QUERY="UPDATE ${DB_TABLE} SET status = '7', callNumber = '${CALL__DEPTH}', requestStatus = '0', requestTime = NOW(), lastErrorMessage = 'screenshot path creation failed', screenshotPathCreateError = '${RETCODE}' WHERE id = '${DB_ID}'"
CHECK_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
print_message "database update error: ${CHECK_RESULT}, QUERY: ${SQL_QUERY}" >> ${ERRLOG}
exit 9
fi
exit 5
fi
mkdir -p "${MAKEPATH}.thumbs/"
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
exec 0<&5 5<&-
rm -f "${TMPFILE}" > /dev/null 2>&1
print_message "mkdir error, return code '${RETCODE}', thumbnail path '${MAKEPATH}.thumbs/'" >> ${ERRLOG}
SQL_QUERY="UPDATE ${DB_TABLE} SET status = '7', callNumber = '${CALL__DEPTH}', requestStatus = '0', requestTime = NOW(), lastErrorMessage = 'screenshot thumb path creation failed', thumbPathCreateError = '${RETCODE}' WHERE id = '${DB_ID}'"
CHECK_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
print_message "database update error: ${CHECK_RESULT}, QUERY: ${SQL_QUERY}" >> ${ERRLOG}
exit 9
fi
exit 6
fi
HEADERSIZE=`/usr/bin/expr $HEADERSIZE`
BODYSIZE=`/usr/bin/expr $FILESIZE - $HEADERSIZE - 2`
# ############################################################################################
# change user to webserver user, so it is writable from web
# ############################################################################################
chown -R ${WEBSERVER_USER}:${WEBSERVER_GROUP} "${CHOWNPATH}"
# feststellen ob des bild wirklich scho gschossa worde isch
if [ "${SCREENSHOT_STATUS}" == "1" ] ; then
DB_UPDATE_STATUS="1"
ERRORMESSAGE_ADD=
ERROR_FIELD__ADD=""
REQUESTED__IMAGE="${SERVER_PATH}/${IMAGE__PATH}${EINTRAG__ID}_large.jpg"
THUMBNAIL__IMAGE="${SERVER_PATH}/${IMAGE__PATH}.thumbs/${EINTRAG__ID}.jpg"
SCREENSHOT_IMAGE="${SERVER_PATH}/${IMAGE__PATH}${EINTRAG__ID}.jpg"
# denn kaschstriere und den schwanz als bild ablege
# vorheriges l�scha isch net n�tig, da auf j�de Fall �baschrieba wrd
tail -c"${BODYSIZE}" "${TMPFILE}" > "${REQUESTED__IMAGE}"
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
DB_UPDATE_STATUS="2"
ERRORMESSAGE_ADD=", lastErrorMessage = 'tail failed for image creation'"
ERROR_FIELD__ADD=", imageCreateError = '${RETCODE}'"
print_message "tail error, return code '${RETCODE}', image '${REQUESTED__IMAGE}'" >> ${ERRLOG}
else
print_message "image created '${REQUESTED__IMAGE}'" >> ${LOG}
chown ${WEBSERVER_USER}:${WEBSERVER_GROUP} "${REQUESTED__IMAGE}"
fi
# Thumb-Erzeugung
if [ -f "${THUMBNAIL__IMAGE}" ] ; then
rm -f "${THUMBNAIL__IMAGE}" >/dev/null 2>&1
fi
convert "${REQUESTED__IMAGE}" -resize "${THUMBNAIL_BREITE}" "${THUMBNAIL__IMAGE}" >/dev/null 2>&1
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
DB_UPDATE_STATUS="2"
ERRORMESSAGE_ADD=", lastErrorMessage = 'convert failed for thumb creation'"
ERROR_FIELD__ADD=", thumbCreateError = '${RETCODE}'"
print_message "convert error, return code '${RETCODE}', thumbnail '${THUMBNAIL__IMAGE}'" >> ${ERRLOG}
else
print_message "thumbnail created '${THUMBNAIL__IMAGE}'" >> ${LOG}
chown ${WEBSERVER_USER}:${WEBSERVER_GROUP} "${THUMBNAIL__IMAGE}"
fi
# Screenshot-Erzeugung
if [ -f "${SCREENSHOT_IMAGE}" ] ; then
rm -f "${SCREENSHOT_IMAGE}" >/dev/null 2>&1
fi
convert "${REQUESTED__IMAGE}" -resize "${SCREENSHOT_BREITE}" "${SCREENSHOT_IMAGE}" >/dev/null 2>&1
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
DB_UPDATE_STATUS="2"
ERRORMESSAGE_ADD=", lastErrorMessage = 'convert failed for screenshot creation'"
ERROR_FIELD__ADD=", screenshotCreateError = '${RETCODE}'"
print_message "convert error, return code '${RETCODE}', screenshot '${SCREENSHOT_IMAGE}'" >> ${ERRLOG}
else
print_message "screenshot created '${SCREENSHOT_IMAGE}'" >> ${LOG}
chown ${WEBSERVER_USER}:${WEBSERVER_GROUP} "${SCREENSHOT_IMAGE}"
fi
if [ -z "${ERRORMESSAGE_ADD}" ] ; then
ERRORMESSAGE_ADD=", lastErrorMessage = 'all images created sucessfully'"
DB_UPDATE_STATUS="0"
fi
SQL_QUERY="UPDATE ${DB_TABLE} SET status = '${DB_UPDATE_STATUS}', callNumber = '${CALL__DEPTH}', requestStatus = '1', requestTime = NOW()${ERRORMESSAGE_ADD}${ERROR_FIELD__ADD} WHERE id = '${DB_ID}'"
CHECK_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
exec 0<&5 5<&-
rm -f "${TMPFILE}" > /dev/null 2>&1
print_message "database update error: ${CHECK_RESULT}, QUERY: ${SQL_QUERY}" >> ${ERRLOG}
exit 9
fi
fi
break;
fi
# Berechnung der Headersize in Bytes und auslesen des X-Image-Status ( Schritt 1 )
if [ "${BODYSTARTED}" == "false" ]
then
HEADERSIZE=`/usr/bin/expr $HEADERSIZE + ${#LINE} + 1`
CHECKSTATUS=`echo "${LINE}" | grep -E "^X-Image-Status" | sed -e 's/^X-Image-Status: \([0-9]\+\).*$/\1/g'`
if [ ! -z "${CHECKSTATUS}" ] ; then
print_message "parsed status code '${CHECKSTATUS}'" >> ${LOG}
SCREENSHOT_STATUS="${CHECKSTATUS}"
# wenn screenshot gerade generiert wird, childprocess aufrufen ( der wirds scho richte )
if [ "${SCREENSHOT_STATUS}" == "2" ] ; then
exec 0<&5 5<&-
rm -f "${TMPFILE}" > /dev/null 2>&1
print_message "image request still pending, calling again '${CHECKSTATUS}'" >> ${PCNTLLOG}
SQL_QUERY="UPDATE ${DB_TABLE} SET status = '3', callNumber = '${CALL__DEPTH}', requestStatus = '2', requestTime = NOW(), lastErrorMessage = 'screenshot not ready yet' WHERE id = '${DB_ID}'"
CHECK_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
print_message "database update error: ${CHECK_RESULT}, QUERY: ${SQL_QUERY}" >> ${ERRLOG}
exit 9
fi
$0 $PARAMS --call-number=${CALL__DEPTH_NEXTCALL} --sleep=1 --sleep-time=${SLEEPTIME} &
exit 1
# wenn screenshot net kommen wird ( weil die des saget ) abbruch, weitergange
elif [ "${SCREENSHOT_STATUS}" != "1" ] ; then
exec 0<&5 5<&-
rm -f "${TMPFILE}" > /dev/null 2>&1
print_message "unable to create screenshot for url '${ENCODED_URL}', status code: '${SCREENSHOT_STATUS}'" >> ${ERRLOG}
SQL_QUERY="UPDATE ${DB_TABLE} SET status = '9', callNumber = '${CALL__DEPTH}', requestStatus = '${SCREENSHOT_STATUS}', requestTime = NOW(), lastErrorMessage = 'X-image status unprocessable' WHERE id = '${DB_ID}'"
CHECK_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
print_message "database update error: ${CHECK_RESULT}, QUERY: ${SQL_QUERY}" >> ${ERRLOG}
exit 9
fi
exit 2
fi
fi
fi
done
exec 0<&5 5<&-
if [ "${BODYSTARTED}" == "false" ] ; then
ERRORFILEPATH="${SERVER_PATH}/batchfiles/batchUnprocessable"
ERRORFILENAME="${ERRORFILEPATH}/${EINTRAG__ID}_${CALL__DEPTH}_curl.txt"
mv -f "${TMPFILE}" "${ERRORFILENAME}" > /dev/null 2>&1
print_message "HTTP response body not found in file '${ERRORFILENAME}'" >> ${LOG}
SQL_QUERY="UPDATE ${DB_TABLE} SET status = '4', callNumber = '${CALL__DEPTH}', requestStatus = '${SCREENSHOT_STATUS}', requestTime = NOW(), lastErrorMessage = 'HTTP response body not found in curl response file: ${ERRORFILENAME}' WHERE id = '${DB_ID}'"
CHECK_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
print_message "database update error: ${CHECK_RESULT}, QUERY: ${SQL_QUERY}" >> ${ERRLOG}
exit 9
fi
else
rm -f "${TMPFILE}" > /dev/null 2>&1
fi
exit 0
#!/bin/bash
DB_NAME="media"
DB_USER="media"
DB_PASS="censored"
DB_CALL="mysql ${DB_NAME} --user=${DB_USER} --password=${DB_PASS} --skip-column-names --batch"
DB_TABLE="screenshots"
DELAY=1
SERVER_PATH="/srv/www/vhosts/media/httpdocs"
BATCHFILE_PATH="${SERVER_PATH}/batchfiles"
LOGPATH="${SERVER_PATH}/batchfiles/batchlog"
TRASHLINESPATH="${SERVER_PATH}/batchfiles/batchTrash"
REDO_FILE=/dev/null
RECR_FILE=/dev/null
FETCHER_SKRIPT="/usr/local/sbin/dbfetchScreen.sh"
FETCHER_SKRIPTNAME=`basename ${FETCHER_SKRIPT}`
FETCHER_SKRIPT_LOGBASENAME=`basename ${FETCHER_SKRIPT} .sh`
URL_DECODE_SCRIPT="/usr/local/etc/cron.d/urldecode.php"
PID_FILE="/var/run/screens/screens.pid"
if [ -f "${PID_FILE}" ] ; then
echo "$(date +[%Y-%m-%s\ %H:%M:%S]) PID file found, ${0} already running"
exit
fi
touch ${PID_FILE}
COMPARE_DATE=`date -d -${DELAY}hours +%s`
FILE_LIST=`ls -1 ${BATCHFILE_PATH}/batchfile_2[0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]_[0-2][0-9] 2>/dev/null | sort`
echo -n "Starte screenshot grabber cron job "
date
if [ ! -z "${FILE_LIST}" ] ; then
for FILEPATH in ${FILE_LIST}
do
FILESIZE=`cat "${FILEPATH}" | wc -c`
if [ ${FILESIZE} == "0" ] ; then
rm -f "${FILEPATH}" >/dev/null 2>&1
continue
fi
BATCHFILE=`basename "${FILEPATH}"`
CHECK=`echo "${BATCHFILE}" | grep -E "batchfile_2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])_([01][0-9]|2[0-3])"`
if [ -n "${CHECK}" ] ; then
unset PARSED_DATA
declare -a PARSED_DATA
PARSED_DATA=( `echo "${BATCHFILE}" | sed -r -e 's/batchfile_(2[0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])_([01][0-9]|2[0-3])/\1\n\2\n\3\n\4/g'` )
if [ "${#PARSED_DATA[@]}" != "4" ] ; then
echo "can not determine file date for file ${FILEPATH}"
unset PARSED_DATA
continue
fi
FILE__YEAR=${PARSED_DATA[0]}
FILE_MONTH=${PARSED_DATA[1]}
FILE___DAY=${PARSED_DATA[2]}
FILE__HOUR=${PARSED_DATA[3]}
DATE_FROM_FILE=`date -d ${FILE__YEAR}-${FILE_MONTH}-${FILE___DAY}\ ${FILE__HOUR}:00:00 +%s`
if [ ${DATE_FROM_FILE} -lt ${COMPARE_DATE} ] ; then
TEMP_WORKING_FILE=`mktemp -t batchfile_${FILE__YEAR}-${FILE_MONTH}-${FILE___DAY}_${FILE__HOUR}-XXXXXXXXXXXXXXXX`
# get only unique lines
# cat "${FILEPATH}" | sort | uniq > ${TEMP_WORKING_FILE}
cat "${FILEPATH}" > ${TEMP_WORKING_FILE}
GENERAL_LOG_FILE_NAME="${BATCHFILE}.log"
GENERAL_LOG="${LOGPATH}/${GENERAL_LOG_FILE_NAME}"
# check log file
if [ -f "${GENERAL_LOG}" ] ; then
rm -f "${GENERAL_LOG}" >/dev/null 2>&1
fi
touch "${GENERAL_LOG}"
if [ "$?" != "0" ] ; then
echo "could not create log file '${GENERAL_LOG}'"
rm -f ${PID_FILE} >/dev/null 2>&1
exit 99
fi
TRASHFILE="${TRASHLINESPATH}/${BATCHFILE}.trash"
CURRENT_DATE=`date`
echo -e "Start batch file processing (${BATCHFILE}), ${CURRENT_DATE}\n" >> ${GENERAL_LOG}
LINENUM=0
exec 4<&0
exec < ${TEMP_WORKING_FILE}
DELIMITER="\\|"
while read LINE
do
DUMMY=$((LINENUM++))
unset PARSED_DATA
declare -a PARSED_DATA
CHECK=`echo "${LINE}" | grep -E "((-)?[1-9][0-9]+)([[:space:]]|\\|)(([^/]+/)+([0-9]{2}/){6}[^/]+/)([[:space:]]|\\|)http(s)?://(.+)"`
if [ -n "${CHECK}" ] ; then
PARSED_DATA=( `echo "${LINE}" | sed -e "s/ /%20/g" | sed -r -e 's/(-?[1-9][0-9]+)[\t|]((([^\/]+\/)+([0-9]{2}\/){5})[0-9]{2}\/[^\/]+\/)(\t|\|)(http(s)?:\/\/[^|]+)/\1\n\2\n\3\n\7/g'` )
EINTRAG__ID=${PARSED_DATA[0]}
IMAGE__PATH=${PARSED_DATA[1]}
OWNER__PATH=${PARSED_DATA[2]}
REQUEST_URL=${PARSED_DATA[3]}
# PARSED_DATA=( `echo "${LINE}" | sed -e "s/ /%20/g" | sed -r -e 's/((-)?[1-9][0-9]+)(\t|\|)(([^\/]+\/)+([0-9]{2}\/){6}[^\/]+\/)(\t|\|)(http(s)?:\/\/[^|]+)/\1\n\4\n\8/g'` )
# EINTRAG__ID=${PARSED_DATA[0]}
# IMAGE__PATH=${PARSED_DATA[1]}
# REQUEST_URL=${PARSED_DATA[2]}
else
CHECK=`echo "${LINE}" | grep -E "((-)?[1-9][0-9]+)([[:space:]]|\\|)http(s)?://(.+)([[:space:]]|\\|)(([^/]+/)+([0-9]{2}/){6}[^/]+/)"`
if [ -n "${CHECK}" ] ; then
PARSED_DATA=( `echo "${LINE}" | sed -e "s/ /%20/g" | sed -r -e 's/(-?[1-9][0-9]+)[\t|](http(s)?:\/\/[^|]+)[\t|]((([^\/]+\/)+([0-9]{2}\/){5})[0-9]{2}\/[^\/]+\/)/\1\n\4\n\5\n\2/g'` )
EINTRAG__ID=${PARSED_DATA[0]}
IMAGE__PATH=${PARSED_DATA[1]}
OWNER__PATH=${PARSED_DATA[2]}
REQUEST_URL=${PARSED_DATA[3]}
# PARSED_DATA=( `echo "${LINE}" | sed -e "s/ /%20/g" | sed -r -e 's/((-)?[1-9][0-9]+)(\t|\|)(http(s)?:\/\/[^|]+)(\t|\|)(([^\/]+\/)+([0-9]{2}\/){6}[^\/]+\/)/\1\n\4\n\7/g'` )
# EINTRAG__ID=${PARSED_DATA[0]}
# IMAGE__PATH=${PARSED_DATA[2]}
# REQUEST_URL=${PARSED_DATA[1]}
else
echo "Line splitting failed" >> ${TRASHFILE}
echo "${LINE}" >> ${TRASHFILE}
continue
fi
fi
# Kunde-ID aus dem Pfad extrahieren
unset PARSED_DATA
declare -a PARSED_DATA
PARSED_DATA=( `echo "${IMAGE__PATH}" | sed -r -e 's/^([^\/]+\/)+(00|11)\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/[^\/]+\/$/\2\n\3\4\5\6\7/g'` )
if [ "${#PARSED_DATA[@]}" == "2" ] ; then
CUSTOMER_ID=`/usr/bin/expr \( -1 + \( ${PARSED_DATA[0]} = 0 \) \* 2 \) \* ${PARSED_DATA[1]}`
else
CUSTOMER_ID=0
fi
printf "Zeile %d, Kunde %d, Eintrag: %d, Url: %s" \
"${LINENUM}" \
"${CUSTOMER_ID}" \
"${EINTRAG__ID}" \
"${REQUEST_URL}" >> ${GENERAL_LOG}
# idna-to-ascii can not convert too long URLs
# Example: http://www.adecco.de/channels/adecconewvi_de/informationandcontacts/branchlocatorde1.asp
# Solution: split URL and path
CHECK=`echo "${REQUEST_URL}" | grep -E "^http(s)?://([^/?]+)([/?].*)?\$"`
if [ -n "${CHECK}" ] ; then
unset PARSED_DATA
declare -a PARSED_DATA
PARSED_DATA=( `echo "${REQUEST_URL}" | sed -r -e 's/^(http(s)?:\/\/)([^\/?]+)([\/?].*)?/\1 \3 \4/g'` )
REQUEST_PROTOCOL="${PARSED_DATA[0]}"
REQUEST___DOMAIN=`php5 ${URL_DECODE_SCRIPT} "${PARSED_DATA[1]}"`
if [ ${#PARSED_DATA[@]} == 3 ] ; then
REQUEST_____PATH="${PARSED_DATA[2]}"
else
REQUEST_____PATH=""
fi
CONVERTED_DOMAIN=`php5 ${URL_DECODE_SCRIPT} ${REQUEST___DOMAIN}`
# zusaetzliche URLs erzeugen
LATIN_____DOMAIN=`echo "${REQUEST___DOMAIN}" | recode UTF-8..ISO-8859-1 2>&1`
RETCODE=$?
if [ "${RETCODE}" == "0" ] ; then
ENCODED___DOMAIN=`CHARSET=ISO-8859-1 idn --idna-to-ascii --tld --quiet "${LATIN_____DOMAIN}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" == "0" ] ; then
CHECK=`echo "${ENCODED___DOMAIN}" | grep -E "^([0-9A-Za-z\\-]+)(\\.[0-9A-Za-z\\-]+)*(\\.[a-zA-Z]{2,8})\$"`
if [ -z "${CHECK}" ] ; then
echo -e "\n\tERROR: Invalid URL '${REQUEST_URL}'\n" >> ${GENERAL_LOG}
echo "URL check after encoding failed: request '${REQUEST___DOMAIN}', latin '${LATIN_____DOMAIN}', encoded '${ENCODED___DOMAIN}'" >> ${TRASHFILE}
echo "${LINE}" >> ${TRASHFILE}
continue
fi
else
echo -e "\n\tERROR: idna-to-ascii failed: latin '${LATIN_____DOMAIN}' --> encoded '${ENCODED___DOMAIN}' (requested: '${REQUEST___DOMAIN}')\n" >> ${GENERAL_LOG}
echo "idna encoding failed, '${LATIN_____DOMAIN}' --> '${ENCODED___DOMAIN}', requested: '${REQUEST___DOMAIN}'" >> ${TRASHFILE}
ENCODED___DOMAIN=`CHARSET=UTF-8 idn --idna-to-ascii --tld --quiet "${REQUEST___DOMAIN}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" == "0" ] ; then
echo -e "\n\tERROR: idna-to-ascii successful for '${REQUEST___DOMAIN}' --> encoded '${ENCODED___DOMAIN}' (latin: '${LATIN_____DOMAIN}')\n" >> ${TRASHFILE}
else
echo -e "\n\tERROR: idna-to-ascii also failed for '${REQUEST___DOMAIN}' --> encoded '${ENCODED___DOMAIN}' (latin: '${LATIN_____DOMAIN}')\n" >> ${TRASHFILE}
fi
echo "${LINE}" >> ${TRASHFILE}
continue
fi
else
echo -e "\n\tERROR: domain name recode from UTF-8 to ISO-8859-1 failed (requested: '${REQUEST___DOMAIN}')\n" >> ${GENERAL_LOG}
echo "recode failed, request '${REQUEST___DOMAIN}', latin '${LATIN_____DOMAIN}'" >> ${TRASHFILE}
echo "${LINE}" >> ${TRASHFILE}
continue
fi
else
echo -e "\n\tERROR: Invalid URL format '${REQUEST_URL}'\n" >> ${GENERAL_LOG}
echo "invalid URL format for requested domain '${REQUEST___DOMAIN}'" >> ${TRASHFILE}
echo "${LINE}" >> ${TRASHFILE}
continue
fi
UTF8____URL="${REQUEST_PROTOCOL}${REQUEST___DOMAIN}${REQUEST_____PATH}"
LATIN___URL="${REQUEST_PROTOCOL}${LATIN_____DOMAIN}${REQUEST_____PATH}"
ENCODED_URL="${REQUEST_PROTOCOL}${ENCODED___DOMAIN}${REQUEST_____PATH}"
if [ "${UTF8____URL}" != "${ENCODED_URL}" ] ; then
echo -e " --> encoded '${ENCODED_URL}'" >> ${GENERAL_LOG}
fi
echo "" >> ${GENERAL_LOG}
SQL_QUERY="SELECT id, status FROM ${DB_TABLE} WHERE eintrag_id = '${EINTRAG__ID}'"
QUERY_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" | grep -E "(-?[1-9][0-9]*)[[:space:]]+-?([0-9]+)"`
RESULT_______ID="invalid"
ERRORS=
if [ -z "${QUERY_RESULT}" ] ; then
SQL_QUERY="INSERT INTO ${DB_TABLE} ( kunde_id, eintrag_id, imagePath, ownerPath, url, encodedUrl, latinUrl, batchFile, created, login_id ) VALUES ( '${CUSTOMER_ID}', '${EINTRAG__ID}', '${IMAGE__PATH}', '${OWNER__PATH}', '${UTF8____URL}', '${ENCODED_URL}', '${LATIN___URL}', '${BATCHFILE}', NOW(), -20001 ); SELECT LAST_INSERT_ID()"
INSERT_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
echo -e "\tERROR: Database insert failed for Eintrag-ID '${EINTRAG__ID}': ${INSERT_RESULT}\n" >> ${GENERAL_LOG}
ERRORS=1
else
RESULT_______ID="${INSERT_RESULT}"
echo -e "\tInserted database entry, ID: ${RESULT_______ID}\n" >> ${GENERAL_LOG}
fi
else
unset PARSED_DATA
declare -a PARSED_DATA
PARSED_DATA=( `echo "${QUERY_RESULT}"` )
if [ "${#PARSED_DATA[@]}" == "2" ] ; then
RESULT_______ID="${PARSED_DATA[0]}"
RESULT___STATUS="${PARSED_DATA[1]}"
if [ "${RESULT___STATUS}" != '9' ] ; then
SQL_QUERY="UPDATE ${DB_TABLE} SET status = '-1', callNumber = '0', requestStatus = '-1', requestTime = '0000-00-00 00:00:00', num404 = ( num404 + 1 ), kunde_id = '${CUSTOMER_ID}', eintrag_id = '${EINTRAG__ID}', imagePath = '${IMAGE__PATH}', ownerPath = '${OWNER__PATH}', url = '${UTF8____URL}', encodedUrl = '${ENCODED_URL}', latinUrl = '${LATIN___URL}', batchFile = '${BATCHFILE}', lastErrorMessage = NULL, recursionError = '0', curlError = '0', screenshotPathCreateError = '0', thumbPathCreateError = '0', imageCreateError = '0', screenshotCreateError = '0', thumbCreateError = '0' WHERE id = '${RESULT_______ID}'"
UPDATE_RESULT=`${DB_CALL} --execute="${SQL_QUERY}" 2>&1`
RETCODE=$?
if [ "${RETCODE}" != "0" ] ; then
echo -e "\tERROR: Database update failed for ID '${RESULT_______ID}': ${UPDATE_RESULT}\n" >> ${GENERAL_LOG}
ERRORS=1
else
echo -e "\tUpdated database entry, ID: ${RESULT_______ID}\n" >> ${GENERAL_LOG}
fi
fi
fi
unset PARSED_DATA
fi
done
exec 0<&4 4<&-
CURRENT_DATE=`date`
echo -e "\nbatch file ${BATCHFILE} processed, ${CURRENT_DATE}\n" >> ${GENERAL_LOG}
# remove the temporary working file
rm -f "${TEMP_WORKING_FILE}" >/dev/null 2>&1
# we have processed this file --> remove it
mv -f "${FILEPATH}" "${BATCHFILE_PATH}/batchProcessed/${BATCHFILE}" >/dev/null 2>&1
fi
fi
done
fi
PROCNUM=`ps aux | grep "${FETCHER_SKRIPTNAME}" | grep -v "grep" | wc -l`
if [ ${PROCNUM} -gt 0 ] ; then
echo "skip processing, because there are still some ${FETCHER_SKRIPTNAME} processes running"
else
SQL_QUERY="SELECT id, batchFile FROM ${DB_TABLE} WHERE status = '-1' ORDER BY batchFile ASC, id ASC"
DELIMITER=";;;;REPLACED_TAB_DELIMITER;;;;"
SPACEREPLACER=""
unset QUERY_RESULT
declare -a QUERY_RESULT
echo "${DB_CALL} --execute=\"${SQL_QUERY}\"";
QUERY_RESULT=( `${DB_CALL} --execute="${SQL_QUERY}" | sed -e "s/\\t/${DELIMITER}/g" | grep -E "((-)?[1-9][0-9]*)${DELIMITER}batchfile_(2[0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])_([01][0-9]|2[0-3])"` )
if [ ${#QUERY_RESULT[@]} -gt 0 ] ; then
echo "process ${#QUERY_RESULT[@]} database entries"
# String fuer das PCNTL Logfile
DATESTRING=`date +%Y-%m-%d_%H`
# process control log file
PCNTL_LOG_FILE_NAME="pcntl.${DATESTRING}.log"
PCNTL_LOG="${LOGPATH}/${PCNTL_LOG_FILE_NAME}"
if [ -f "${PCNTL_LOG}" ] ; then
rm -f "${PCNTL_LOG}" >/dev/null 2>&1
fi
touch "${PCNTL_LOG}"
if [ "$?" != "0" ] ; then
echo "could not create process control log file '${PCNTL_LOG}'"
rm -f ${PID_FILE} >/dev/null 2>&1
exit 99
fi
for (( i=0 ; i<${#QUERY_RESULT[@]} ; i++ ))
do
LOOPS=0
PROCNUM=`ps aux | grep "${FETCHER_SKRIPTNAME}" | grep -v "grep" | wc -l`
while [ ${PROCNUM} -gt 100 ]
do
DUMMY=$((LOOPS++))
PROCNUM=`ps aux | grep "${FETCHER_SKRIPTNAME}" | grep -v "grep" | wc -l`
sleep 1
done
if [ "${LOOPS}" != "0" ] ; then
printf "Zeile %d, Kunde %d, Eintrag: %d, %d Sekunden Wartezeit vor dem Skript-Aufruf\n" \
"${LINENUM}" \
"${KD_ID}" \
"${EINTRAG_ID}" \
"${LOOPS}" >> ${PCNTL_LOG}
fi
LINE=${QUERY_RESULT[${i}]}
unset PARSED_DATA
declare -a PARSED_DATA
PARSED_DATA=( `echo ${LINE} | sed -r -e "s/([0-9\\-]+)${DELIMITER}(.+)/\1 \2/g"` )
if [ "${#PARSED_DATA[@]}" == "2" ] ; then
DBENTRY__ID="${PARSED_DATA[0]}"
BATCH__FILE="${PARSED_DATA[1]}"
fi
SUBPROCESS_______LOG_FILE_NAME="${BATCH__FILE}.screenshots.log"
SUBPROCESS_PCNTL_LOG_FILE_NAME="${BATCH__FILE}.screenshotsPcntl.log"
SUBPROCESS_ERROR_LOG_FILE_NAME="${BATCH__FILE}.screenshotsError.log"
SUBPROCESS_______LOG="${LOGPATH}/${SUBPROCESS_______LOG_FILE_NAME}"
SUBPROCESS_PCNTL_LOG="${LOGPATH}/${SUBPROCESS_PCNTL_LOG_FILE_NAME}"
SUBPROCESS_ERROR_LOG="${LOGPATH}/${SUBPROCESS_ERROR_LOG_FILE_NAME}"
# check sub process log file
touch "${SUBPROCESS_______LOG}"
if [ "$?" != "0" ] ; then
echo "could not create sub process log file '${SUBPROCESS_______LOG}'"
rm -f ${PID_FILE} >/dev/null 2>&1
exit 99
fi
# check sub process pcntl log file
touch "${SUBPROCESS_PCNTL_LOG}"
if [ "$?" != "0" ] ; then
echo "could not create sub process process control log file '${SUBPROCESS_PCNTL_LOG}'"
rm -f ${PID_FILE} >/dev/null 2>&1
exit 99
fi
# check sub process error log file
touch "${SUBPROCESS_ERROR_LOG}"
if [ "$?" != "0" ] ; then
echo "could not create sub process error log file '${SUBPROCESS_ERROR_LOG}'"
rm -f ${PID_FILE} >/dev/null 2>&1
exit 99
fi
# --database-name=
# --database-user=
# --database-pass=
# --database-table=
# --database-id=
# --log-path=
# --log-file=
# --error-log-file=
# --pcntl-log-file=
# --server-path=
# --sleep-time=
# --sleep=
# --call-number=
##################################### DEBUG
# printf "${FETCHER_SKRIPT} %s %s %s %s %s %s %s %s %s %s %s\n" \
# "--database-name=${DB_NAME}" \
# "--database-user=${DB_USER}" \
# "--database-pass=${DB_PASS}" \
# "--database-table=${DB_TABLE}" \
# "--database-id=${DBENTRY__ID}" \
# "--log-path=${LOGPATH}" \
# "--log-file=${SUBPROCESS_______LOG_FILE_NAME}" \
# "--error-log-file=${SUBPROCESS_ERROR_LOG_FILE_NAME}" \
# "--pcntl-log-file=${SUBPROCESS_PCNTL_LOG_FILE_NAME}" \
# "--server-path=${SERVER_PATH}" \
# "--dummy=dummy"
##################################### DEBUG
${FETCHER_SKRIPT} \
--database-name=${DB_NAME} \
--database-user=${DB_USER} \
--database-pass=${DB_PASS} \
--database-table=${DB_TABLE} \
--database-id=${DBENTRY__ID} \
--log-path=${LOGPATH} \
--log-file=${SUBPROCESS_______LOG_FILE_NAME} \
--error-log-file=${SUBPROCESS_ERROR_LOG_FILE_NAME} \
--pcntl-log-file=${SUBPROCESS_PCNTL_LOG_FILE_NAME} \
--server-path=${SERVER_PATH} \
--dummy=dummy
done
else
echo "no entries from query"
fi
fi
rm -f "${PID_FILE}" >/dev/null 2>&1
echo -n "Beende screenshot grabber cron job "
date
echo ""
exit
<?php
if ( $argc > 1 )
{
echo rawurldecode( $argv[1] ) . "\n";
exit;
}
echo "\n";
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment