|
#!/bin/bash |
|
set -E -o pipefail |
|
|
|
MAILTO="${MAILTO:-root}" |
|
|
|
# prepare report |
|
REPOS=() |
|
REPOS_REPORT=() |
|
ARCHIVES_REPORT=() |
|
|
|
ARCHIVE_SIZE_SCRIPT='' |
|
ARCHIVE_SIZE_SCRIPT+='/^ +Original size +Compressed size +Deduplicated size$/ ' |
|
ARCHIVE_SIZE_SCRIPT+='{ LENGTH=length($0); sub(/^ +/, ""); LENGTH-=length($0); getline; print substr($0, LENGTH + 1) }' |
|
|
|
REPO_INFO_SCRIPT='' |
|
REPO_INFO_SCRIPT+='/^ +Original size +Compressed size +Deduplicated size$/ ' |
|
REPO_INFO_SCRIPT+='{ exit } NR > 1 { print LINE } { LINE=$0 }' |
|
|
|
REPO_SIZE_SCRIPT='' |
|
REPO_SIZE_SCRIPT+='/^ +Original size +Compressed size +Deduplicated size$/ ' |
|
REPO_SIZE_SCRIPT+='{ PRINT=1 } PRINT { print }' |
|
|
|
# handle archives files |
|
# replace known archives file on success or |
|
# delete temporary file on failure |
|
ARCHIVES_FILES=() |
|
ARCHIVES_FILES_NEW=() |
|
|
|
function trap_failure { |
|
local RC=$? |
|
|
|
for ARCHIVES_FILE_NEW in "${ARCHIVES_FILES_NEW[@]}"; do |
|
rm -f "$ARCHIVES_FILE_NEW" |
|
done |
|
|
|
trap - INT TERM ERR EXIT |
|
[ $RC -ne 0 ] || RC=1 |
|
exit $RC |
|
} |
|
|
|
function trap_success { |
|
INDEX=0 |
|
while [ $INDEX -lt ${#ARCHIVES_FILES[@]} ]; do |
|
mv "${ARCHIVES_FILES_NEW[$INDEX]}" "${ARCHIVES_FILES[$INDEX]}" |
|
((++INDEX)) |
|
done |
|
|
|
trap - INT TERM ERR EXIT |
|
exit 0 |
|
} |
|
|
|
trap 'trap_failure' INT TERM ERR |
|
trap 'trap_success' EXIT |
|
|
|
# create reports |
|
BORG_EXEC=( "borg" ) |
|
BORG_REPO="" |
|
for REPO_FILE in /etc/borg-report/*; do |
|
REPO_NAME="$(basename "$REPO_FILE")" |
|
REPOS+=( "$REPO_NAME" ) |
|
|
|
# include repo file |
|
. "$REPO_FILE" |
|
[ -n "$BORG_REPO" ] || { echo "Invalid repo config file '$REPO_FILE': Invalid repository path" >&2; exit 1; } |
|
|
|
# check known archives file |
|
ARCHIVES_FILE="/var/cache/borg-report/$REPO_NAME" |
|
if [ -e "$ARCHIVES_FILE" ] || [ -h "$ARCHIVES_FILE" ]; then |
|
[ -f "$ARCHIVES_FILE" ] || { echo "Unable to read archives file '$ARCHIVES_FILE': No such file" >&2; exit 1; } |
|
[ -r "$ARCHIVES_FILE" ] || { echo "Unable to read archives file '$ARCHIVES_FILE': Permission denied" >&2; exit 1; } |
|
else |
|
touch "$ARCHIVES_FILE" |
|
fi |
|
|
|
# get a list of not-yet-seen archives |
|
ARCHIVES_FILE_NEW="$(mktemp)" |
|
|
|
ARCHIVES_FILES+=( "$ARCHIVES_FILE" ) |
|
ARCHIVES_FILES_NEW+=( "$ARCHIVES_FILE_NEW" ) |
|
|
|
"${BORG_EXEC[@]}" list --short "$BORG_REPO" > "$ARCHIVES_FILE_NEW" |
|
|
|
readarray -t ARCHIVES < <( |
|
diff --new-line-format='%L' --old-line-format='' --unchanged-line-format='' \ |
|
"$ARCHIVES_FILE" "$ARCHIVES_FILE_NEW" \ |
|
|| true |
|
) |
|
LATEST_ARCHIVE="$(tail -n 1 "$ARCHIVES_FILE_NEW")" |
|
|
|
# create archives report |
|
ARCHIVE_SIZES=() |
|
for ARCHIVE in "${ARCHIVES[@]}"; do |
|
ARCHIVE_SIZE="$("${BORG_EXEC[@]}" info "$BORG_REPO::$ARCHIVE" | awk "$ARCHIVE_SIZE_SCRIPT")" |
|
ARCHIVE_SIZES+=( "$(echo "$ARCHIVE_SIZE" | awk -F ' +' '{ print $2"|"$3"|"$4 }')|$ARCHIVE" ) |
|
done |
|
|
|
ARCHIVES_REPORT+=( "$(printf "%s\n" "${ARCHIVE_SIZES[@]}")" ) |
|
|
|
# create repo report |
|
LATEST_ARCHIVE_INFO="$("${BORG_EXEC[@]}" info "$BORG_REPO::$LATEST_ARCHIVE")" |
|
|
|
REPO_REPORTS+=( "$( |
|
printf -- '-%.0s' {1..80}; echo |
|
awk "$REPO_INFO_SCRIPT" <<< "$LATEST_ARCHIVE_INFO" |
|
printf -- '-%.0s' {1..80}; echo |
|
awk "$REPO_SIZE_SCRIPT" <<< "$LATEST_ARCHIVE_INFO" |
|
printf -- '-%.0s' {1..80}; echo |
|
"${BORG_EXEC[@]}" list "$BORG_REPO" |
|
)" ) |
|
done |
|
|
|
# send notification email |
|
SUBJECT="Borg <$(id -un)@$(hostname)> Activity Notification" |
|
|
|
OUTPUT=() |
|
OUTPUT+=( '<!DOCTYPE HTML>' ) |
|
OUTPUT+=( '<html lang="en">' ) |
|
OUTPUT+=( ' <head>' ) |
|
OUTPUT+=( ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8">' ) |
|
OUTPUT+=( ' <meta name="viewport" content="width=device-width">' ) |
|
OUTPUT+=( " <title>$SUBJECT</title>" ) |
|
OUTPUT+=( ' <style type="text/css">*{box-sizing:border-box;border:0;margin:0;padding:0}body,html{height:100%;background:#fff;font-family:Lucida Grande,Geneva,Verdana,sans-serif}#header,#main{width:100%;padding:0 .5em}#header{background:#428bca}.container{max-width:48em;margin:0 auto;padding:1em}#main .container{margin:4em auto;background:#f3f3f3}h1{padding:1.5em 0;color:#fff}h2{padding:0 0 .5em;margin:0 0 1em;border-bottom:1px solid #ccc;font-size:1.5rem}.content{padding:.5em 1em;background:#fff}.content pre,.content .table-wrapper{overflow-x:auto}table{border-spacing:0;min-width:100%}th{font-weight:bold;text-align:center;border-bottom:1px solid #ccc}td,th{padding:.25em .5em;background:#fff}td:first-child{white-space:nowrap;background:#f3f3f3}td:not(:first-child){min-width:9em;text-align:right}td:not(:last-child),th:not(:last-child){border-right:1px solid #ccc}tbody tr:last-child td:first-child{border-bottom-left-radius:5px}code{font-family:Lucida Console,Monaco,Courier New,monospace;font-size:.9em}h2 code{padding:.1em .2em;vertical-align:middle;background:#fff}#main .container,.content,h2 code,#main{border:1px solid #ccc;border-radius:5px}</style>' ) |
|
OUTPUT+=( ' </head>' ) |
|
OUTPUT+=( ' <body>' ) |
|
OUTPUT+=( ' <div id="header">' ) |
|
OUTPUT+=( ' <div class="container">' ) |
|
OUTPUT+=( ' <h1>Borg Activity Notification</h1>' ) |
|
OUTPUT+=( ' </div>' ) |
|
OUTPUT+=( ' </div>' ) |
|
OUTPUT+=( ' <div id="main">' ) |
|
|
|
for (( INDEX=0; INDEX < ${#REPOS[@]}; INDEX++ )); do |
|
[ -n "${ARCHIVES_REPORT[$INDEX]}" ] || continue |
|
|
|
OUTPUT+=( ' <div class="container">' ) |
|
OUTPUT+=( " <h2>Repository <code>${REPOS[$INDEX]}</code> - Recent Archives</h2>" ) |
|
OUTPUT+=( ' <div class="content">' ) |
|
OUTPUT+=( ' <div class="table-wrapper">' ) |
|
OUTPUT+=( ' <table>' ) |
|
OUTPUT+=( ' <thead>' ) |
|
OUTPUT+=( ' <tr>' ) |
|
OUTPUT+=( ' <th>Archive</th>' ) |
|
OUTPUT+=( ' <th>Original size</th>' ) |
|
OUTPUT+=( ' <th>Compressed size</th>' ) |
|
OUTPUT+=( ' <th>Deduplicated size</th>' ) |
|
OUTPUT+=( ' </tr>' ) |
|
OUTPUT+=( ' </thead>' ) |
|
OUTPUT+=( ' <tbody>' ) |
|
|
|
while IFS= read -r LINE; do |
|
OUTPUT+=( ' <tr>' ) |
|
OUTPUT+=( " <td><code>$(echo "$LINE" | cut -d '|' -f 4-)</code></td>" ) |
|
OUTPUT+=( " <td>$(echo "$LINE" | cut -d '|' -f 1)</td>" ) |
|
OUTPUT+=( " <td>$(echo "$LINE" | cut -d '|' -f 2)</td>" ) |
|
OUTPUT+=( " <td>$(echo "$LINE" | cut -d '|' -f 3)</td>" ) |
|
OUTPUT+=( ' </tr>' ) |
|
done <<< "${ARCHIVES_REPORT[$INDEX]}" |
|
|
|
OUTPUT+=( ' </tbody>' ) |
|
OUTPUT+=( ' </table>' ) |
|
OUTPUT+=( ' </div>' ) |
|
OUTPUT+=( ' </div>' ) |
|
OUTPUT+=( ' </div>' ) |
|
done |
|
|
|
for (( INDEX=0; INDEX < ${#REPOS[@]}; INDEX++ )); do |
|
OUTPUT+=( ' <div class="container">' ) |
|
OUTPUT+=( " <h2>Repository <code>${REPOS[$INDEX]}</code> - Overview</h2>" ) |
|
OUTPUT+=( ' <div class="content">' ) |
|
OUTPUT+=( " <pre><code>${REPO_REPORTS[$INDEX]}</code></pre>" ) |
|
OUTPUT+=( ' </div>' ) |
|
OUTPUT+=( ' </div>' ) |
|
done |
|
|
|
OUTPUT+=( ' </div>' ) |
|
OUTPUT+=( ' </body>' ) |
|
OUTPUT+=( '</html>' ) |
|
|
|
printf '%s\n' "${OUTPUT[@]}" | mail -a "Content-Type: text/html; charset=utf-8" -s "$SUBJECT" "$MAILTO" |