config / .fgfs / fgaddon /
e7f8f8e 4 years ago
1 contributor
436 lines | 16.355kb
#!/bin/bash

set -e

declare -A data=(
        [/sim/description]=text
        [/sim/long-description]=text
        [/sim/author]=text
        [/sim/flight-model]=text
        [/sim/type]=text
        [/sim/model/path]=text
)
fgaddon_svn=https://svn.code.sf.net/p/flightgear/fgaddon/trunk/Aircraft
fgaddon_path=$HOME/.fgfs/flightgear-fgaddon/Aircraft
database=${DB:-$0.db}
#locale=fr

test -r "$0.conf" && source $0.conf && echo config red

aircrafts=$(mktemp --dry-run /dev/shm/Aircraft-XXXXXXXXX)
aircraft=$(mktemp --dry-run /dev/shm/aircraft-XXXXXXX)
setxml=$(mktemp --dry-run /dev/shm/setxml-XXXXXXXX)
in_ram_database=$(mktemp --dry-run /dev/shm/XXXXXXX)

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
}

rm -f /dev/shm/sqlite_request
function sqlite_request () {
    local delimiter=';'
    test ${1:0:1} == '.' && delimiter=''
    echo "${1}${delimiter}" >> /dev/shm/sqlite_request
    if ! sqlite3 "$in_ram_database" <<< "$1"; then
        register_state
    fi
}

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//' $setxml # removes BOM
}

function trap_break () {
    trap '' INT
    echo "stop requested"
    register_state
}

function trap_exit () {
    trapped_rc=$?
    trap '' INT
    rm -f $aircrafts $aircraft $setxml
    if test ! -e $in_ram_database; then
        exit
    fi
    test $trapped_rc -ne 0 && register_state
    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 $fgaddon_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 $fgaddon_path/*/*-set.xml; do
        ac=${ac/$fgaddon_path}
        sx=${ac##*/}
        ac=${ac%/*}
        if test -d $fgaddon_path/$ac/.svn; then
            install_type=1
        elif test -d $fgaddon_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', name)
                                     from aircrafts
                                     where id not in (select variantof from setxml)")
    if test -n "$missing_setxml"; then
        echo -e "missing setxml config for :\n$missing_setxml"
    fi
    local missing_model=$(sqlite_request 'select count(setxml.file)
                                          from aircrafts inner join setxml
                                          where aircrafts.id = setxml.variantof and setxml.`/sim/model/path` = ""')
    if test $missing_model -gt 0; then
        echo "$missing_model aircrafts without /sim/model/path information"
        if test $missing_model -le 10; then
            echo "aircrafts without /sim/model/path information:"
            sqlite_request 'select printf(" - %s/%s", aircrafts.name, setxml.file)
                            from aircrafts inner join setxml
                            where aircrafts.id = setxml.variantof and setxml.`/sim/model/path` = ""'
        fi
    fi
    if test -r "$database" && md5sum $in_ram_database | sed "s,$in_ram_database,$database," | md5sum --status -c -; then
        rm -f $in_ram_database
        echo "no changes in $database"
    elif test -w "$database"; then
        sqlite_request "vacuum"
        mv -f $in_ram_database "$database"
        echo "database $database updated"
    elif ! test -e "$database"; then
        mv $in_ram_database "$database"
        echo "database $database created"
    else
        rm -f $in_ram_database
        echo "nothing can be done with $database !"
    fi
}

function register_state () {
    sqlite_request "drop table if exists recover_rev"
    sqlite_request "create table recover_rev (
                        revkey text,
                        revision integer,
                        revauthor text,
                        revdate integer
                    )"
    for revkey in ${!revindex[@]}; do
        sqlite_request "insert into recover_rev values (
                            '$revkey',
                            ${revindex[$revkey]:-0},
                            '${revauthor[$revkey]}',
                            ${revdate[$revkey]:-0}
                        )"
    done
    sqlite_request "drop table if exists recover_setxmlmodified"
    sqlite_request "create table if not exists recover_setxmlmodified (
                        sx text
                    )"
    for sx in ${!setxmlmodified[@]}; do
        sqlite_request "insert into recover_setxmlmodified values (
                            '$sx'
                        )"
    done
    exit
}

function update_database () {
    dbupdate=$(sqlite_request "select revision from aircrafts where name is '${ac:1}'")
    if test -z "$dbupdate"; then
        sqlite_request "insert into aircrafts (name, revision, date, author)
                        values ('${ac:1}', ${revindex[$ac]}, ${revdate[$ac]}, '${revauthor[$ac]}')"
    elif test $dbupdate -lt ${revindex[$ac]}; then
        sqlite_request "update aircrafts set
                            revision = ${revindex[$ac]},
                            author   = '${revauthor[$ac]}',
                            date = ${revdate[$ac]}
                        where name is '${ac:1}'"
    fi
    id=$(sqlite_request "select id from aircrafts where name is '${ac:1}'")

    echo "[ ${#revindex[@]} ] ${ac:1}"
    unset revindex[$ac]

    for sx in ${!setxmlmodified[@]}; do
        unset include include_rootpath
        [[ "$sx" =~ ^"${ac:1}/" ]] || continue
        for col in ${!data[@]}; do
            data[$col]=
        done
        sx=${sx#*/}
        echo " -> $sx"
        if ! svn cat $fgaddon_svn/${ac:1}/$sx-set.xml > $setxml; then
            register_state
        fi
        xmlremovecomments
        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 [[ "$TAG" =~ ^"PropertyList include=" ]]; then
                include_rootpath=${include%/*}
                test $include = $include_rootpath && unset include_rootpath
                eval $(echo ${TAG#* })
                [[ "$include" =~ ^Aircraft/Generic/ ]] && unset include include_rootpath && continue
                if [[ "$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
                if ! svn cat $fgaddon_svn/${ac:1}/${include_rootpath:+$include_rootpath/}$include >> $setxml; then
                    register_state
                fi
                xmlremovecomments
            fi

            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 ${!data[@]}; do
                test -z "${data[$col]}" && continue 2
            done
            break # everything's found
        done < $setxml

        if eval "test -z \"$data_test_null\""; then
            echo "WARNING: no info found, skipping"
            continue
        fi

        known=$(sqlite_request "select variantof from setxml where file is '$sx'")
        if test -n "$known"; then
            for col in ${!data[@]}; do
                dbvalue=$(sqlite_request "select \`$col\`
                                          from setxml
                                          where file is '$sx' and variantof = $known")
                if test "$dbvalue" != "${data[$col]}" -a -n "${data[$col]}"; then
                    sqlite_request "update setxml
                                    set \`$col\` = '${data[$col]//\'/\'\'}'
                                    where file is '$sx' and variantof = $known"
                fi
            done
        else
            values="'$sx', $id, "
            for col in ${!data[@]}; do
                values+="'${data[$col]//\'/\'\'}', "
            done
            values+=0
            sqlite_request "insert into setxml values ($values)"
        fi
        test -n "$ac_save" && ac=$ac_save
        unset setxmlmodified[${ac:1}/$sx]
    done
}

function apply_revision () {
    for ac in "${!revindex[@]}"; do
        update_database
        if test -d $fgaddon_path/${ac:1}/.svn \
        && test "$(svn info --show-item=url $fgaddon_path/${ac:1})" != "$fgaddon_svn/${ac:1}" \
        || test -d $fgaddon_path/${ac:1} -a ! -d $fgaddon_path/${ac:1}/.svn; then
            echo "INFO: local ${ac:1} installed out from repo" >&2
        fi
    done
}

trap trap_break INT
trap trap_exit EXIT

stty -echoctl

declare -A revindex revauthor revdate setxmlmodified files revpath
data_pattern=$(printf "%s|" ${!data[@]})
data_pattern=${data_pattern:0:-1}
data_test_null=$(printf '${data[%s]}' ${!data[@]})

if test -e $database; then
    cp $database $in_ram_database
    sql_cols=$(sqlite_request "pragma table_info(setxml)" | awk -F'|' '{printf("%s %s ", $2, $3)}')
    script_cols="file text variantof integer "
    for col in ${!data[@]}; do
        script_cols+="$col ${data["$col"]} "
    done
    script_cols+="installed integer " # last space is important
    if test "$sql_cols" != "$script_cols"; then
        echo "ALERT: datbase version mismatch !"
        exit 1
    fi
    if sqlite_request '.tables' | grep -q 'recover_' && test -z "$1"; then
        echo "recovering from previous saved state"
        eval $(sqlite_request "select printf('revindex[%s]=%u;revauthor[%s]=%s;revdate[%s]=%u;',
                                       revkey, revision,
                                       revkey, revauthor,
                                       revkey, revdate)
                               from recover_rev")
        eval $(sqlite_request "select printf('setxmlmodified[%s]=1;', sx)
                               from recover_setxmlmodified")
        sqlite_request "drop table recover_rev"
        sqlite_request "drop table recover_setxmlmodified"
        apply_revision
        exit
    fi
fi

sqlite_request "create table if not exists aircrafts (
                    id       integer primary key,
                    name     text,
                    revision integer,
                    date     integer,
                    author   text)"

sqlite_request "create table if not exists setxml (
                    file text,
                    variantof integer,
                    $(for col in ${!data[@]}; do printf "'%s' %s, " $col ${data[$col]}; done)
                    installed integer)"

latest_revision=$(( $(sqlite_request "select max(revision) from aircrafts") + 1 ))

# for debugging purpose
if test -n "$2"; then
    ac=_${1%/*}
    revindex[$ac]=1
    revdate[$ac]=0
    revauthor[$ac]=foobar
    setxmlmodified[${ac:1}/${1#*/}]=1
    set -x
    update_database
    set +x
    exit
elif test -n "$1"; then
    ac=_${1%/*}
    eval $(sqlite_request "select printf('revindex[_%s]=%s;revdate[_%s]=%i;revauthor[_%s]=%s;',
                                            name, revision,
                                            name, date,
                                            name, author)
                           from aircrafts
                           where name = '${ac:1}'")
    setxmlmodified[${ac:1}/${1#*/}]=1
    if test -z "${revindex[$ac]}"; then
        echo "aircraft ${ac:1} not found"
        rm $in_ram_database
        exit
    fi
    update_database
    exit
fi

echo "downloading FGADDON history from revision ${latest_revision:-0}"
svn log --revision ${latest_revision:-0}:HEAD --xml --verbose $fgaddon_svn > $aircrafts

total=$(grep -c '<logentry' $aircrafts)
progress=0

echo parsing history

while xmlgetnext; do
    case "$TAG" in
        'logentry revision='*)
            eval $(echo ${TAG#* })
            for action in ${!revpath[@]}; do
                unset revpath[$action]
            done
        ;;
        'author')
            revauthor=${VALUE//\'/\'\'}
        ;;
        'date')
            revdate=$(date +%s -d "$VALUE")
        ;;
        'path '*)
            TAG=${TAG#* }
            TAG=${TAG// /;}
            TAG=${TAG//-/_}
            eval $(echo ${TAG// /;})
            path=(${VALUE//\// })
            if test $kind = 'file' -a ${#path[@]} -gt 3; then
                revpath[$action]+="$VALUE "
            elif test $kind = 'dir' -a ${#path[@]} -eq 3 -a $action = 'D'; then
                files[_${path[2]}]=0
                unset revindex[_${path[2]}] revauthor[_${path[2]}] revdate[_${path[2]}]
                for sx in ${!setxmlmodified[@]}; do
                    [[ "$sx" =~ "${path[2]}/" ]] && unset setxmlmodified[$sx]
                done
            fi
        ;;
        '/logentry')
            for item in ${revpath[D]}; do
                path=(${item//\// })
                if [[ "${path[3]}" =~ "-set.xml" ]]; then
                    unset setxmlmodified[${path[2]}/${path[3]/-set.xml}]
                    sqlite_request "delete from setxml where file = '${path[3]/-set.xml}'"
                fi
                files[_${path[2]}]=$(( --files[_${path[2]}] ))
                if test ${files[_${path[2]}]} -le 0; then
                    unset revindex[_${path[2]}] revauthor[_${path[2]}] revdate[_${path[2]}]
                    sqlite_request "delete from aircrafts where name = '${path[2]}'"
                fi
            done
            for action in A M R; do
                for item in ${revpath[$action]}; do
                    path=(${item//\// })
                    revindex[_${path[2]}]=$revision
                    revauthor[_${path[2]}]=$revauthor
                    revdate[_${path[2]}]=$revdate
                    [[ "${path[3]}" =~ "-set.xml" ]] && setxmlmodified[${path[2]}/${path[3]/-set.xml}]=1
                    test $action = 'A' && files[_${path[2]}]=$(( ++files[_${path[2]}] ))
                done
            done
            newprogress=$((++logentry * 100 / $total))
            if test $(( $newprogress - $progress )) -ge ${progress_granularity:-1}; then
                progress=$newprogress
                echo "$progress% (${#revindex[@]})"
            fi
        ;;
        '/log')
            apply_revision
            break
        ;;
    esac
done < $aircrafts