#!/bin/bash set -e declare -A datatypes=( [/sim/description]=text [/sim/long-description]=text [/sim/author]=text [/sim/flight-model]=text [/sim/type]=text [/sim/model/path]=text [/sim/rating/FDM]="integer DEFAULT 0" [/sim/rating/systems]="integer DEFAULT 0" [/sim/rating/cockpit]="integer DEFAULT 0" [/sim/rating/model]="integer DEFAULT 0" ) missing_data_check=( /sim/model/path ) database=${DB:-$0.db} test -r "$0.conf" && source "$0.conf" #locale=fr tempid=$(mktemp --dry-run XXXXXXX) temppath=/dev/shm setxml=$temppath/setxml-$tempid json_file=$temppath/github_json-$tempid in_ram_database=$temppath/${database##*/}-$tempid function xmlgetnext () { local IFS='>' read -d '<' TAG VALUE # by design, the first TAG/VALUE pair is empty # to avoid infinite loops at end of file parsing we return an error # the next time we find an empty TAG if test -z "$TAG"; then test ${xmlgetnext_empty_tag:-0} -gt 0 && return 1 xmlgetnext_empty_tag=$(( xmlgetnext_empty_tag + 1 )) fi # process $TAG only if necessary local _TAG=$(printf '%q' $TAG) if test ${_TAG:0:1} = '$'; then TAG=$(tr '\n' ' ' <<< $TAG | sed 's/ */ /g; s/ *$//') fi } function json () { jq --raw-output "$1" < ${2:-${json_file:?}} } rm -f $temppath/sqlite_request function sqlite_request () { echo -e "## REQ $(( ++sqlite_request_count ))\n${1}\n" >> $temppath/sqlite_request sqlite3 "$in_ram_database" <<< "$1" } function xmlremovecomments () { sed -ri 's/<(!--|script>)/\n&/;s/(<\/script|--)>/&\n/' $setxml sed -ri '/<(script>|!--).*(<\/script|--)>/d;/<(script>|!--)/,/(<\/script|--)>/d' $setxml sed -i 's/\xef\xbb\xbf//;s/\r//' $setxml # removes BOM and ^M } function trap_break () { trap '' INT echo "stop requested" } function trap_exit () { trapped_rc=$? trap '' INT if declare -f on_exit > /dev/null; then on_exit fi if test ! -e $in_ram_database; then exit fi echo "updating installation status" for ac in $(sqlite_request 'select printf("%i:%s/%s", aircrafts.id, aircrafts.name, setxml.file) from aircrafts inner join setxml where aircrafts.id = setxml.variantof and setxml.installed != 0;'); do ac_path=${ac#*:} if test ! -e ${hangar[path]}/$ac_path-set.xml; then sqlite_request "update setxml set installed = 0 where file = '${ac_path#*/}' and variantof = ${ac%:*}" fi done for ac in ${hangar[path]}/*/*-set.xml; do ac=${ac/${hangar[path]}} sx=${ac##*/} ac=${ac%/*} if test -d ${hangar[path]}/$ac/.svn; then install_type=1 elif test -d ${hangar[path]}/$ac/.git; then install_type=2 else install_type=3 fi sqlite_request "update setxml set installed = $install_type where exists ( select 1 from aircrafts where name = '${ac/\/}' and setxml.variantof = id )" done local missing_setxml=$(sqlite_request "select printf(' - %s (%s)', aircrafts.name, hangars.name) from aircrafts inner join hangars where hangars.id = aircrafts.hangar and aircrafts.id not in (select variantof from setxml)") if test -n "$missing_setxml"; then echo -e "missing setxml config for :\n$missing_setxml" fi for data_presence_check in ${missing_data_check[@]}; do if [[ -v datatypes[$data_presence_check] ]]; then local missing_data=$(sqlite_request "select count(setxml.file) from aircrafts inner join setxml where aircrafts.id = setxml.variantof and setxml.\`$data_presence_check\` = ''") if test $missing_data -gt 0; then echo "$missing_data aircrafts without $data_presence_check information" if test $missing_data -le 10; then echo "aircrafts without $data_presence_check information:" sqlite_request "select printf(' - %s/%s (%s)', aircrafts.name, setxml.file, hangars.name) from aircrafts inner join setxml, hangars where aircrafts.id = setxml.variantof and aircrafts.hangar = hangars.id and setxml.\`$data_presence_check\` = ''" fi fi fi done if test -r "$database" && sqldiff=$(sqldiff $in_ram_database $database) && test -z "$sqldiff"; then echo "no changes in $database" elif test -w "$database"; then rm -f "$database" sqlite_request '.dump' | sqlite3 "$database" echo "database $database updated" elif test ! -e "$database" -a -w ${database%/*}; then sqlite_request '.dump' | sqlite3 "$database" echo "database $database created" else echo "nothing can be done with $database !" fi find $temppath -type f -name "*-$tempid" -delete } function update_database () { sqlite_request "insert into aircrafts (name, author, revision, date, hangar) select name, author, revision, date, hangar from recover_aircrafts where recover_aircrafts.name = '$ac' and recover_aircrafts.hangar = ${hangar[id]} on conflict (name, hangar) where aircrafts.name = '$ac' and aircrafts.hangar = ${hangar[id]} do update set author = (select author from recover_aircrafts where name = '$ac'), revision = (select revision from recover_aircrafts where name = '$ac'), date = (select date from recover_aircrafts where name = '$ac') where aircrafts.name = '$ac' and aircrafts.hangar = ${hangar[id]}" id=$(sqlite_request "select id from aircrafts where name is '${ac}' and hangar = ${hangar[id]}") echo $(sqlite_request "select printf('[ %i/%i ] $ac', count(sx), count(distinct ac)) from recover_setxml") for sx in $(sqlite_request "select distinct sx from recover_setxml where ac = '$ac'"); do unset data declare -A data printf " -> $sx" getfromrepo ${ac}/$sx-set.xml > $setxml unset xmlgetnext_empty_tag property include include_rootpath ac_save while xmlgetnext; do if [[ "$TAG" =~ ^"PropertyList include=" ]]; then include_rootpath=${include%/*} test $include = $include_rootpath && unset include_rootpath eval $(echo ${TAG#* }) # include="..." if [[ "$include" =~ ^Aircraft/Generic/ ]]; then unset include include_rootpath continue elif [[ "$include" =~ ^'../' ]]; then if test -n "$include_rootpath"; then if [[ "$include_rootpath" =~ '/' ]]; then include_rootpath=${include_rootpath%/*} else unset include_rootpath fi else ac_save=$ac unset ac fi include=${include/\.\.\/} fi getfromrepo ${ac}/${include_rootpath:+$include_rootpath/}$include >> $setxml fi done < $setxml test -n "$ac_save" && ac=$ac_save # some aircrafts (mostly from the helijah's files architecture template) # break because of infinite loop in middle of file # I can't find the reason of this infinite loop # this is the reason of this double-pass unset xmlgetnext_empty_tag property while xmlgetnext; do case "${TAG:0:1}" in ''|'?'|'!') continue;; /) property=${property%/*};; *) if test "${TAG: -1}" != '/'; then property+=/${TAG%% *} fi;; esac if [[ "$property" = /PropertyList@($data_pattern) ]]; then if test -z "${data[${property/\/PropertyList}]}"; then eval "data[${property/\/PropertyList}]=\"${VALUE//\"/\\\"}\"" data[${property/\/PropertyList}]=$(tr '\n' ' ' <<< ${data[${property/\/PropertyList}]} | sed -r 's/^\s*//;s/\s+/ /g;s/\s*$//') fi fi # continue parsing (while loop) until everything's found for col in ${!datatypes[@]}; do test -z "${data[$col]}" && continue 2 done break # everything's found done < $setxml if eval "test -z \"$data_test_null\""; then printf "\nWARNING: no info found, skipping\n" mkdir -p $temppath/no-data-ac cp -f $setxml $temppath/no-data-ac/${ac}-${sx} else insert_values="'$sx', $id, " insert_col='file, variantof, ' update_values='' for col in ${!data[@]}; do if test ${datatypes[$col]%% *} = 'text'; then single_quote="'" elif [[ ${datatypes[$col]%% *} = 'integer' && "${data[$col]// }" = +([0-9]) ]]; then single_quote="" else unset datatypes[$col] continue fi insert_col+="\`$col\`, " insert_values+="$single_quote${data[$col]//\'/\'\'}$single_quote, " if test -n "${data[$col]}"; then update_values+="\`$col\` = $single_quote${data[$col]//\'/\'\'}$single_quote, " fi done local flag_new= local flag_status= if test $(sqlite_request "select count(file) from setxml where file = '$sx' and variantof = $id") -eq 0; then flag_new="NEW" fi for criteria in FDM model systems cockpit; do if test ${data[/sim/rating/$criteria]:-0} -ge 4; then flag_status+='*' fi done if test -n "$flag_new" -o -n "$flag_status"; then printf " (${flag_new:+$flag_new }$flag_status)" fi printf "\n" sqlite_request "insert into setxml (${insert_col%,*}, installed) values (${insert_values%,*}, 0) on conflict (file, variantof) where file = '$sx' and variantof = $id do update set ${update_values%,*}, installed = 0 where file = '$sx' and variantof = $id" fi sqlite_request "delete from recover_setxml where ac = '$ac' and sx = '$sx'" done } function add_record () { ac_ass_array[$1]="$2" } function get_record () { if test -n "$1"; then echo "${ac_ass_array[$1]}" else for k in ${!ac_ass_array[@]}; do echo $k = ${ac_ass_array[$k]} done fi } function add_aircraft () { for key in name revision date author; do test -n "${ac_ass_array[$key]}" # exit if missing data (with the help of "set -e") done local new_revision=$(sqlite_request "select revision from recover_aircrafts where name = '${ac_ass_array[name]}'") if test -z "${new_revision}"; then sqlite_request "insert into recover_aircrafts (name, revision, date, author, hangar) values ( '${ac_ass_array[name]}', ${ac_ass_array[revision]}, ${ac_ass_array[date]}, '${ac_ass_array[author]}', ${hangar[id]})" elif test ${new_revision} -lt ${ac_ass_array[revision]//\"}; then sqlite_request "update recover_aircrafts set revision = ${ac_ass_array[revision]}, date = ${ac_ass_array[date]}, author = '${ac_ass_array[author]}', hangar = ${hangar[id]} where name = '${ac_ass_array[name]}'" fi for key in name revision date author; do ac_ass_array[$key]='' done } function add_setxml_for_aircraft () { sqlite_request "insert into recover_setxml values ('$1', '${2/%-set.xml}') on conflict (ac, sx) where ac = '$1' and sx = '${2/%-set.xml}' do nothing" } function apply_revision () { for ac in $(sqlite_request "select name from recover_aircrafts"); do # delete aircrafts that have been deleted from the repo sqlite_request "delete from setxml where (file, variantof) in ( select file, variantof from setxml inner join aircrafts where aircrafts.id = setxml.variantof and aircrafts.name = '$ac' and aircrafts.hangar = ${hangar[id]} and setxml.file not in ( select sx from recover_setxml where ac = '$ac' ) )" # delete aircrafts without setxml found sqlite_request "delete from recover_aircrafts where name not in (select distinct ac from recover_setxml)" update_database if test -d ${hangar[path]}/${ac}/.${hangar[type]} \ && \ case ${hangar[type]} in svn) test "$(svn info --show-item=url ${hangar[path]}/${ac})" != "${hangar[url]}/${ac}";; git) test "$(git -C ${hangar[path]}/${ac} config --get remote.origin.url)" != "${hangar[url]}/${ac}.git";; esac \ || test -d ${hangar[path]}/${ac} -a ! -d ${hangar[path]}/${ac}/.${hangar[type]}; then echo "INFO: local ${ac} installed out from repo" >&2 fi sqlite_request "delete from recover_aircrafts where name = '$ac'" done } trap trap_break INT trap trap_exit EXIT stty -echoctl declare -A hangar data_pattern=$(printf "%s|" ${!datatypes[@]}) data_pattern=${data_pattern:0:-1} data_test_null=$(printf '${data[%s]}' ${!datatypes[@]}) if test -e $database; then cp $database $in_ram_database sql_cols=$(sqlite_request "pragma table_info(setxml)" \ | cut --delimiter="|" --fields=2,3 --output-delimiter=" " \ | sort \ | md5sum) datatypes[file]=text datatypes[variantof]=integer datatypes[installed]=integer script_cols=$(for col in ${!datatypes[@]}; do echo "$col ${datatypes["$col"]%% *}"; done \ | sort \ | md5sum) unset datatypes[file] datatypes[variantof] datatypes[installed] if test "$sql_cols" != "$script_cols"; then echo "ALERT: datbase version mismatch !" exit 1 fi if sqlite_request '.tables' | grep -q 'recover_'; then hangar[id]=$(sqlite_request "select hangar from recover_aircrafts limit 1") if test -n "${hangar[id]}"; then echo "recovering from previous saved state" eval $(sqlite_request "select printf('hangar[name]=%s;hangar[url]=%s;hangar[type]=%s;hangar[source]=%s', name, url, type, source) from hangars where id = '${hangar[id]}'") source $(grep -l "^\s*hangar\[name\]=${hangar[name]}\s*$" ${0%*/}.d/*.hangar) eval "getfromrepo () {$(declare -f getfromrepo | sed '1,2d;$d'); xmlremovecomments;}" apply_revision exit else sqlite_request 'drop table recover_aircrafts' sqlite_request 'drop table recover_setxml' fi fi fi sqlite_request "create table if not exists hangars ( id integer primary key, name text, source text, type text, url text, path text, active integer)" sqlite_request 'create unique index if not exists "index_hangars" on hangars (url)' sqlite_request "create table if not exists aircrafts ( id integer primary key, name text, revision integer, date integer, author text, hangar integer)" sqlite_request 'create unique index if not exists "index_aircrafts" on aircrafts (name, hangar)' sqlite_request "create table if not exists setxml ( file text, variantof integer, $(for col in ${!datatypes[@]}; do printf "'%s' %s, " $col "${datatypes[$col]}"; done) installed integer)" sqlite_request 'create unique index if not exists "index_setxml" on setxml (file, variantof)' for file in $(find ${0%*/}.d -type f -name "*.hangar"); do unset hangar error_message unset -f getfromrepo parse_repo_history declare -A hangar source $file test -n "${hangar[name]}" \ -a -n "${hangar[source]}" \ -a -n "${hangar[type]}" \ -a -n "${hangar[url]}" \ -a -n "${hangar[active]}" || \ error_message="${error_message:+$error_message, }missing hangar data" declare -f getfromrepo > /dev/null || \ error_message="${error_message:+$error_message, }missing getfromrepo function" declare -f parse_repo_history > /dev/null || \ error_message="${error_message:+$error_message, }missing parse_repo_history function" if test -n "$error_message"; then echo "file $file isn't a valid hangar ($error_message)" continue fi sqlite_request "insert into hangars (name, source, type, url, path, active) values ( '${hangar[name]}', '${hangar[source]}', '${hangar[type]}', '${hangar[url]}', '${hangar[path]}', ${hangar[active]}) on conflict (url) where url = '${hangar[url]}' do update set name = '${hangar[name]}', path = '${hangar[path]}', active = ${hangar[active]} where url = '${hangar[url]}'" done unset hangar unset -f getfromrepo parse_repo_history declare -A hangar ac_ass_array for h_id in $(sqlite_request "select id from hangars where active = 1"); do sqlite_request 'create table if not exists recover_aircrafts ( name text, revision integer, date integer, author text, hangar integer)' sqlite_request 'create table if not exists recover_setxml ( ac text, sx text)' sqlite_request 'create unique index if not exists "index_recover_setxml" on recover_setxml (ac, sx)' eval $(sqlite_request "select printf('hangar[id]=%i;hangar[source]=%s;', id, source) from hangars where id = '${h_id}'") source $(grep -l "^\s*hangar\[source\]=${hangar[source]}\s*$" ${0%*/}.d/*.hangar) eval "getfromrepo () {$(declare -f getfromrepo | sed '1,2d;$d'); xmlremovecomments;}" echo -e "=${hangar[name]//?/=}=\n ${hangar[name]} \n=${hangar[name]//?/=}=" latest_revision=$(( $(sqlite_request "select max(revision) from aircrafts inner join hangars where hangars.id = aircrafts.hangar and hangars.name = '${hangar[name]}'") + 1 )) parse_repo_history if declare -f on_exit > /dev/null; then on_exit fi sqlite_request "drop index 'index_recover_setxml'" sqlite_request "drop table recover_aircrafts" sqlite_request "drop table recover_setxml" done