#!/bin/bash
#
# Copyright(C) 2024 @ ZENETYS
# This script is licensed under MIT License (http://opensource.org/licenses/MIT)
#

set -f
export LC_ALL=C
umask 0077
PROGNAME=${0##*/}
DAYS=
QUIET=
DRY_RUN=
LOCK="/dev/shm/$PROGNAME.lock"
ZLC_XZ_OPTS=( -9 )
ZLC_ENV=${ZLC_ENV:-/etc/logcenter/logcenter.conf}

function exit_usage() {
    local status=${1:-0}
    [[ "$status" != "0" ]] && exec >&2
    echo "\
Usage: $PROGNAME [-n] [-q] [-d DAYS]
Compress log files collected via syslog

Available options:
  -d, --days    NUM     Keep n day(s) of uncompressed files.
  -q, --quiet           Display errors only.
  -n, --dry-run         Dry-run mode, do not change anything.
  -h, --help            Display this help."
    exit "$status"
}

function _log() {
    local now=$(date +%Y-%m-%dT%H:%M:%S.%3N%:z)
    local LOGGER_SEVERITY=${LOGGER_SEVERITY:-info}
    local STDOUT_LABEL=${STDOUT_LABEL:-INFO}
    [[ -t 0 ]] || logger -t "$PROGNAME[$$]" -p "$LOGGER_SEVERITY" -- "${DRY_RUN:+DRY-RUN ** }$*"
    [[ -n $QUIET && $STDOUT_LABEL == INFO ]] && return 0
    echo "$now $STDOUT_LABEL $PROGNAME: ${DRY_RUN:+DRY-RUN ** }$*"
}

function info() { LOGGER_SEVERITY=info STDOUT_LABEL=INFO _log "$@"; }
function warning() { LOGGER_SEVERITY=warning STDOUT_LABEL=WARNING _log "$@"; }
function error() { LOGGER_SEVERITY=err STDOUT_LABEL=ERROR _log "$@"; }
function fatal() { LOGGER_SEVERITY=crit STDOUT_LABEL=FATAL _log "$@"; exit 2; }

function release_lock() {
    rm "$LOCK"
}

function acquire_lock_or_fatal() {
    if ! ln -sT "/proc/$$" "$LOCK"; then
        [[ -e "$LOCK" ]] || release_lock
        ln -sT "/proc/$$" "$LOCK" ||
            fatal "Failed to acquire lock $LOCK"
    fi
}

function load_env_or_fatal() {
    # proper $PATH may be needed for sigfiled
    [[ -f /etc/profile.d/logcenter-path.sh ]] && source /etc/profile.d/logcenter-path.sh
    source "$ZLC_ENV" || fatal "Failed to source $ZLC_ENV"
    [[ -z $ZLC_ARCHIVES_DIR ]] && fatal 'Bad environment, ZLC_ARCHIVES_DIR not set'
    [[ -d $ZLC_ARCHIVES_DIR/. ]] || fatal 'Invalid ZLC_ARCHIVES_DIR, not a directory'
    [[ -n $ZLC_SIGFILED_SOCKNAME ]] && export ${!ZLC_SIGFILED_*}
    DAYS=$ZLC_ARCHIVES_UNCOMPRESSED_DAYS # may be overriden by script argument
    return 0
}

[[ $UID == 0 ]] ||
    fatal 'This script must be run as root'

load_env_or_fatal

while (( $# > 0 )); do
    case "$1" in
        -d|--days) DAYS=$2; shift ;;
        -q|--quiet) QUIET=1 ;;
        -n|--dry-run) DRY_RUN=1 ;;
        -h|--help) exit_usage ;;
        *) exit_usage 1 ;;
    esac
    shift
done

[[ -n $DAYS && -z ${DAYS//[0-9]} ]] ||
    fatal 'Invalid uncompressed days, not a number'

acquire_lock_or_fatal
trap release_lock EXIT

current_log_yyyy_mm_dd=$(date +%Y-%m-%d)
if [[ -z $current_log_yyyy_mm_dd ]]; then
    fatal "Call to date failed, cannot to get current date, abort"
fi

find_opts=( -mindepth 3 -maxdepth 3 -type f )
if (( DAYS == 0 )); then
    # safeguard, need an mtime of 60 minutes minimum
    # meaning that task is better scheduled after 1am
    find_opts+=( -mmin +60 )
else
    find_opts+=( -mtime "+$DAYS" )
fi
find_opts+=(
    -name "*$ZLC_SELF_ID_EXT.log"
    -not -name "${current_log_yyyy_mm_dd}${ZLC_SELF_ID_EXT}.log"
)

total=0
success=0
error=0
warning=0

time_main_start=$(date +%s)

info "BEGIN TASK -- ZLC_ARCHIVES_DIR=${ZLC_ARCHIVES_DIR:-<empty>}, \
ZLC_SELF_ID_EXT=${ZLC_SELF_ID_EXT:-<empty>}, ZLC_XZ_OPTS=${ZLC_XZ_OPTS[*]:-<empty>}, \
DAYS=${DAYS:-<empty>}, DRY_RUN=${DRY_RUN:-<empty>}, QUIET=${QUIET:-<empty>}"
info "Find options: ${find_opts[*]}"

while read -r; do
    xfiletmp="$REPLY.xz.partial$RANDOM"

    info "Processing $REPLY"
    (( total++ ))

    if [[ -n $DRY_RUN ]]; then
        info "DRY-RUN enabled, skip to next"
        continue
    fi

    if [[ -n $ZLC_SIGFILED_SOCKNAME ]]; then
        # Sign the file.
        # This is best effort, as compressing the file is the main operation
        # here. Monitoring of unsigned logs must be done by another process.
        SECONDS=0
        sigdir="${REPLY%/*}/sig"
        sigfname="${REPLY##*/}${ZLC_SELF_ID_EXT}.sig"
        if mkdir -p -m 0700 "$ZLC_ARCHIVES_DIR/$sigdir" &&
           sigout=$(sigfiled sign "$ZLC_ARCHIVES_DIR/$REPLY" "$ZLC_ARCHIVES_DIR/$sigdir/$sigfname"); then

            info "Signing $REPLY done in ${SECONDS}s${sigout:+, $sigout}"
        else
            warning "Signing $REPLY failed${sigout:+, $sigout}"
            (( warning++ ))
        fi
    fi

    # Compress the file.
    SECONDS=0
    nice -n 19 xz "${ZLC_XZ_OPTS[@]}" ${QUIET:+-q} \
        < "$ZLC_ARCHIVES_DIR/$REPLY" \
        > "$ZLC_ARCHIVES_DIR/$xfiletmp"

    if (( $? == 0 )); then
        (( success++ ))
        info "Done compressing $REPLY in ${SECONDS}s"
    else
        (( error++ ))
        error "Call to xz failed, error compressing $REPLY"
        rm "$ZLC_ARCHIVES_DIR/$xfiletmp"
        continue
    fi

    # Restore user/group, perms, mtime.
    chown --reference "$ZLC_ARCHIVES_DIR/$REPLY" "$ZLC_ARCHIVES_DIR/$xfiletmp"
    if (( $? != 0 )); then
        warning "Call to chown failed on $xfiletmp"
        (( warning++ ))
    fi
    chmod --reference "$ZLC_ARCHIVES_DIR/$REPLY" "$ZLC_ARCHIVES_DIR/$xfiletmp"
    if (( $? != 0 )); then
        warning "Call to chmod failed on $xfiletmp"
        (( warning++ ))
    fi
    touch --reference "$ZLC_ARCHIVES_DIR/$REPLY" "$ZLC_ARCHIVES_DIR/$xfiletmp"
    if (( $? != 0 )); then
        warning "Call to touch (mtime) failed on $xfiletmp"
        (( warning++ ))
    fi

    # Remove the original .log file now so we minimize chances to
    # have both a .log.xz and .log at the same time on the FS.
    #
    # At worse,
    #   - if rm and mv fails, we have the .log and the .log.xz.partial
    #   - if only rm fails, we have the .log and the .log.xz
    #   - if only mv fails, we have le .log.xz.partial
    #
    # Any case, we still have the data.

    rm "$ZLC_ARCHIVES_DIR/$REPLY"
    if (( $? != 0 )); then
        warning "Call to rm failed on $REPLY"
        (( warning++ ))
    fi 

    xfile="$REPLY.xz"
    if [[ -e $ZLC_ARCHIVES_DIR/$xfile ]]; then
        for (( i = 1 ; ; i++ )); do
            if [[ ! -e $ZLC_ARCHIVES_DIR/$REPLY.$i.xz ]]; then
                xfile="$REPLY.$i.xz"
                break
            fi
        done
    fi
    mv "$ZLC_ARCHIVES_DIR/$xfiletmp" "$ZLC_ARCHIVES_DIR/$xfile"
    if (( $? != 0 )); then
        warning "Call to mv failed, could not rename $xfiletmp to ${xfile##*/}"
        (( warning++ ))
    fi

done < <(
    find -L "$ZLC_ARCHIVES_DIR/" "${find_opts[@]}" -printf '%P\n' |sort -V
)

time_main_stop=$(date +%s)
message="END TASK -- elapsed: $(( time_main_stop - time_main_start ))s, \
files: $total, success: $success, error: $error, warning: $warning"

if (( error > 0 )); then
    error "$message"
    exit 1
elif (( warning > 0 )); then
    warning "$message"
    exit 0
else
    info "$message"
    exit 0
fi
