unzip -tq can block indefinitely on very large or partially corrupt ZIP archives (stalls in I/O rather than exiting with a CRC error). All three scripts that call validate_recovered_file() are affected: recover_, restore_, recreate_bad_signature.sh. Both the quick check (unzip -tq) and the verbose error pass (unzip -t) are now wrapped with `timeout 120`. Exit code 124 (timed out) is reported as UNVERIFIED with a hint for manual follow-up; any other non-zero exit is still reported as INVALID with the first error line.
1718 lines
62 KiB
Bash
Executable File
1718 lines
62 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
CUR_IFS=$IFS
|
|
|
|
script_name="$(basename $(realpath $0))"
|
|
script_dir="$(dirname $(realpath $0))"
|
|
|
|
conf_dir="${script_dir}/conf"
|
|
snippet_dir="${script_dir}/snippets"
|
|
report_dir="${script_dir}/reports"
|
|
|
|
# - Source of the files this script writes back into Nextcloud: the
|
|
# - already-recovered, already-validated copies produced by a PRIOR
|
|
# - recover_bad_signature.sh run (same default as that script - see the
|
|
# - comment there for why it lives outside script_dir/root).
|
|
# -
|
|
DEFAULT_RECOVERY_BASE_DIR="/var/nc-recovery"
|
|
|
|
# - Before this script overwrites the still-broken original in
|
|
# - Nextcloud's own storage, it keeps a raw, byte-for-byte copy of the
|
|
# - CURRENT on-disk ciphertext (the file exactly as it is right now,
|
|
# - still failing signature checks) in a separate directory - a plain
|
|
# - filesystem copy, independent of Nextcloud/its encryption layer
|
|
# - entirely (which is the whole point: that layer can't "read" this
|
|
# - file cleanly right now anyway). This is a rollback path on top of
|
|
# - (not a replacement for) the already-recovered/validated copies under
|
|
# - DEFAULT_RECOVERY_BASE_DIR.
|
|
# -
|
|
DEFAULT_RESTORE_BACKUP_BASE_DIR="/var/nc-restore-backup"
|
|
|
|
declare -a unsorted_website_arr
|
|
declare -a website_arr
|
|
|
|
declare -a unsorted_account_arr
|
|
declare -a account_arr
|
|
|
|
declare -a unsorted_selected_user_arr
|
|
declare -a selected_user_arr
|
|
|
|
LOCK_DIR="/tmp/${script_name%%.*}.LOCK"
|
|
log_file="${LOCK_DIR}/${script_name%%.*}.log"
|
|
|
|
run_date=$(date +%Y-%m-%d-%H%M)
|
|
|
|
|
|
# =============
|
|
# --- Some functions
|
|
# =============
|
|
|
|
usage() {
|
|
|
|
[[ -n "$1" ]] && error "$1"
|
|
|
|
[[ $terminal ]] && echo -e "
|
|
\033[1mUsage:\033[m
|
|
|
|
$(basename $0) -s <website>
|
|
|
|
\033[1mDescription\033[m
|
|
|
|
Writes already-recovered, already-validated files (produced by a
|
|
PRIOR run of recover_bad_signature.sh) back into Nextcloud's own
|
|
storage, at their ORIGINAL path - through Nextcloud's normal Files
|
|
API (the same write path a regular upload/sync client write uses),
|
|
so the Server-Side Encryption layer transparently (re-)encrypts the
|
|
content with a fresh IV and a correct signature. Afterwards the
|
|
file is a completely normal, healthy encrypted file again: readable
|
|
in the web interface, over WebDAV, and by sync clients, with no
|
|
more 'Bad Signature' and no server-side flag left toggled.
|
|
|
|
This is the ONLY script in this toolkit that writes into
|
|
Nextcloud's live storage - scan_bad_signature.sh and
|
|
recover_bad_signature.sh are both read-only towards Nextcloud
|
|
itself. Treat it accordingly.
|
|
|
|
IMPORTANT:
|
|
- Two categories of entries from the recovery report are
|
|
restored, both by default, no separate flag needed:
|
|
* validation=VALID - a dedicated structural check (PDF
|
|
header, zip integrity, image signature, ...) actually
|
|
confirmed the recovered content.
|
|
* validation=UNVERIFIED ('Nicht pruefbar' in the recovery
|
|
report) - unusual/unrecognized file type, no dedicated
|
|
check exists, but nothing about it looks wrong either
|
|
(recovered/original size ratio is in the expected range,
|
|
or no size comparison applies to that check path). This
|
|
is the 'unauffaellig' case: we cannot prove it's correct,
|
|
but we also have no concrete reason to think it's not.
|
|
Entries where the size ratio actively 'looks off' (a genuine
|
|
negative signal, not just 'unchecked') are treated like
|
|
INVALID and are NEVER restored automatically, same as
|
|
'Datenmuell'/INVALID entries - no override exists for either.
|
|
Every restored UNVERIFIED file is clearly marked as such (its
|
|
own report column, its own counters) so this is always
|
|
transparent afterwards - never silently indistinguishable from
|
|
a properly verified VALID restore. If you'd rather review the
|
|
UNVERIFIED/'Nicht pruefbar' files yourself first, use
|
|
delete_files.sh to remove the ones you don't want kept, run
|
|
this script for 'VALID' coverage, then decide on the rest by
|
|
hand.
|
|
- Every file is re-validated with the CURRENT checks right
|
|
before writing (not just trusted from the old report), in case
|
|
this script's validation logic has improved since that report
|
|
was produced - files that no longer validate (as VALID or as
|
|
plausible UNVERIFIED) are skipped, not forced through.
|
|
- Before each file is overwritten, the CURRENT on-disk ciphertext
|
|
(the still-broken original, exactly as it is right now) is
|
|
copied byte-for-byte to a separate backup directory (default
|
|
${DEFAULT_RESTORE_BACKUP_BASE_DIR}/<website>, override with
|
|
RESTORE_BACKUP_BASE_DIR in the conf file) - a plain filesystem
|
|
copy that bypasses Nextcloud/encryption entirely, so it works
|
|
even though the file cannot currently be read normally. This
|
|
only works for local/default storage; it is best-effort and a
|
|
missing source is reported but does not abort the run.
|
|
- This only reads the already-recovered copies under
|
|
${DEFAULT_RECOVERY_BASE_DIR}/<website> (override with
|
|
RECOVERY_BASE_DIR) - it does NOT decrypt anything itself and
|
|
does NOT touch 'encryption_skip_signature_check'. Run
|
|
recover_bad_signature.sh first if that directory/report is
|
|
missing or stale.
|
|
- After writing, every file is immediately read back through the
|
|
NORMAL Nextcloud read path (no signature-check bypass) and
|
|
compared byte-for-byte (SHA-256) against the source - proof
|
|
that the file is genuinely healed, not just that a write
|
|
happened without error.
|
|
- If the 'files_versions' app is enabled, Nextcloud will also
|
|
keep its own snapshot of the overwritten (still-broken)
|
|
content as a file version automatically - an additional,
|
|
built-in safety net on top of the raw backup above.
|
|
|
|
Unless '-n' is given on the command line, you will be asked
|
|
interactively whether to run a dry-run (nothing is written, just
|
|
reports what would happen) or the real thing. You will then be
|
|
asked which existing recovery report (produced by
|
|
recover_bad_signature.sh) to use, and then which account(s) from its
|
|
restorable (VALID + plausible UNVERIFIED) entries to restore for:
|
|
|
|
- a blank separated list of account names
|
|
- or 'all' to attempt every account found in the report
|
|
|
|
\033[1mOptions\033[m
|
|
|
|
-s <website>
|
|
The site of the nextcloud instance.
|
|
|
|
-n
|
|
Dry-run: goes through selection, re-validation and reporting, but
|
|
does NOT back up, overwrite or verify anything. Passing it here
|
|
skips the interactive mode question mentioned above.
|
|
|
|
\033[1mExample:\033[m
|
|
|
|
Runs this script on system 'cloud-01.oopen.de'
|
|
|
|
$(basename $0) -s cloud-01.oopen.de
|
|
|
|
Dry-run only, nothing is written:
|
|
|
|
$(basename $0) -n -s cloud-01.oopen.de
|
|
"
|
|
|
|
clean_up 1
|
|
|
|
}
|
|
|
|
|
|
clean_up() {
|
|
|
|
# Perform program exit housekeeping
|
|
[[ -n "$restore_php_file" ]] && rm -f "$restore_php_file" 2> /dev/null
|
|
rm -rf "$LOCK_DIR"
|
|
blank_line
|
|
exit $1
|
|
|
|
}
|
|
|
|
is_number() {
|
|
|
|
return $(test ! -z "${1##*[!0-9]*}" > /dev/null 2>&1);
|
|
}
|
|
|
|
echononl(){
|
|
if $terminal ; then
|
|
echo X\\c > /tmp/shprompt$$
|
|
if [ `wc -c /tmp/shprompt$$ | awk '{print $1}'` -eq 1 ]; then
|
|
echo -e -n "$*\\c" 1>&2
|
|
else
|
|
echo -e -n "$*" 1>&2
|
|
fi
|
|
rm /tmp/shprompt$$
|
|
fi
|
|
}
|
|
echo_done() {
|
|
if $terminal ; then
|
|
echo -e "\033[75G[ \033[32mdone\033[m ]"
|
|
fi
|
|
}
|
|
echo_ok() {
|
|
if $terminal ; then
|
|
echo -e "\033[75G[ \033[32mok\033[m ]"
|
|
fi
|
|
}
|
|
echo_warning() {
|
|
if $terminal ; then
|
|
echo -e "\033[75G[ \033[33m\033[1mwarn\033[m ]"
|
|
fi
|
|
}
|
|
echo_failed(){
|
|
if $terminal ; then
|
|
echo -e "\033[75G[ \033[1;31mfailed\033[m ]"
|
|
fi
|
|
}
|
|
echo_skipped() {
|
|
if $terminal ; then
|
|
echo -e "\033[75G[ \033[37mskipped\033[m ]"
|
|
fi
|
|
}
|
|
|
|
fatal (){
|
|
echo ""
|
|
echo ""
|
|
if $terminal ; then
|
|
echo -e " [ \033[31m\033[1mFatal\033[m ]: \033[37m\033[1m$*\033[m"
|
|
echo ""
|
|
echo -e " \033[31m\033[1mScript will be interrupted..\033[m!"
|
|
else
|
|
echo " [ Fatal ]: $*"
|
|
echo ""
|
|
echo " Script was terminated...."
|
|
fi
|
|
clean_up 1
|
|
}
|
|
|
|
error(){
|
|
echo ""
|
|
if $terminal ; then
|
|
echo -e " [ \033[31m\033[1mError\033[m ]: $*"
|
|
else
|
|
echo " [ Error ]: $*"
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
warn (){
|
|
if $terminal ; then
|
|
echo ""
|
|
echo -e " [ \033[33m\033[1mWarning\033[m ]: $*"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
info (){
|
|
if $terminal ; then
|
|
echo ""
|
|
echo -e " [ \033[32m\033[1mInfo\033[m ]: $*"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
# - Remove leading/trailling whitespaces
|
|
# -
|
|
trim() {
|
|
local var="$*"
|
|
var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters
|
|
var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters
|
|
echo -n "$var"
|
|
}
|
|
|
|
## - Check if a given array (parameter 2) contains a given string (parameter 1)
|
|
## -
|
|
containsElement () {
|
|
local e
|
|
for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
|
|
return 1
|
|
}
|
|
|
|
## - Percentage (1 decimal) of parameter 1 (part) against parameter 2 (total)
|
|
## - Returns "0.0" if total is empty, zero or non-numeric.
|
|
## -
|
|
calc_percent() {
|
|
local _part="$1"
|
|
local _total="$2"
|
|
if [[ -z "$_total" ]] || ! [[ "$_total" =~ ^[0-9]+$ ]] || [[ "$_total" -eq 0 ]] ; then
|
|
echo "0.0"
|
|
else
|
|
awk -v b="$_part" -v t="$_total" 'BEGIN { printf "%.1f", (b/t)*100 }'
|
|
fi
|
|
}
|
|
|
|
## - Best-effort structural validation of a recovered file - IDENTICAL
|
|
## - to (and must be kept in sync with) validate_recovered_file() and
|
|
## - its helpers in recover_bad_signature.sh. Duplicated here on
|
|
## - purpose rather than sourced from that script: sourcing it would
|
|
## - also execute its top-level report-selection flow, not just define
|
|
## - functions. Prints "CLASS|detail" - CLASS is one of VALID / INVALID
|
|
## - / UNVERIFIED.
|
|
## -
|
|
validate_recovered_file() {
|
|
local _f="$1" _origpath="$2" _origsize="$3"
|
|
local _base="$(basename -- "$_origpath")"
|
|
local _ext=""
|
|
if [[ "$_base" == *.* ]] ; then
|
|
_ext="${_origpath##*.}"
|
|
_ext="$(echo "$_ext" | tr '[:upper:]' '[:lower:]')"
|
|
fi
|
|
local _size
|
|
_size=$(stat -c%s "$_f" 2> /dev/null)
|
|
[[ -z "$_size" ]] && _size=0
|
|
|
|
case "$_ext" in
|
|
pdf)
|
|
if _pdf_check "$_f" ; then
|
|
echo "VALID|$_pdf_check_detail"
|
|
else
|
|
echo "INVALID|$_pdf_check_detail"
|
|
fi
|
|
;;
|
|
jpg|jpeg)
|
|
if _jpeg_check "$_f" ; then
|
|
echo "VALID|$_jpeg_check_detail"
|
|
else
|
|
echo "INVALID|$_jpeg_check_detail"
|
|
fi
|
|
;;
|
|
png)
|
|
local _head
|
|
_head="$(head -c8 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
if [[ "$_head" = "89504e470d0a1a0a" ]] ; then
|
|
echo "VALID|PNG signature ok"
|
|
else
|
|
echo "INVALID|PNG signature missing"
|
|
fi
|
|
;;
|
|
gif)
|
|
local _head6
|
|
_head6="$(head -c6 "$_f" 2> /dev/null)"
|
|
if [[ "$_head6" = "GIF87a" || "$_head6" = "GIF89a" ]] ; then
|
|
echo "VALID|GIF signature ok"
|
|
else
|
|
echo "INVALID|GIF signature missing"
|
|
fi
|
|
;;
|
|
bmp)
|
|
if [[ "$(head -c2 "$_f" 2> /dev/null)" = "BM" ]] ; then
|
|
echo "VALID|BMP signature ok"
|
|
else
|
|
echo "INVALID|BMP signature missing"
|
|
fi
|
|
;;
|
|
tif|tiff)
|
|
local _head4
|
|
_head4="$(head -c4 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
if [[ "$_head4" = "49492a00" || "$_head4" = "4d4d002a" ]] ; then
|
|
echo "VALID|TIFF signature ok"
|
|
else
|
|
echo "INVALID|TIFF signature missing"
|
|
fi
|
|
;;
|
|
pnm|pgm|ppm|pbm)
|
|
local _head2
|
|
_head2="$(head -c2 "$_f" 2> /dev/null)"
|
|
if [[ "$_head2" =~ ^P[1-6]$ ]] ; then
|
|
echo "VALID|PNM/PGM/PPM magic ok"
|
|
else
|
|
echo "INVALID|PNM/PGM/PPM magic (P1-P6) missing"
|
|
fi
|
|
;;
|
|
odt|ods|odp|odg|odf|ott|ots|otp|otg|docx|xlsx|pptx|docm|xlsm|pptm|dotm|xltm|potm|ppsm|ppsx|zip|epub)
|
|
# A password-protected MS Office file (Standard/Agile encryption,
|
|
# MS-OFFCRYPTO) is NOT a zip at all, regardless of its docx/xlsx/
|
|
# pptx extension: the whole package is wrapped in an OLE2/CFBF
|
|
# compound-file container (the same format legacy .doc/.xls/.ppt
|
|
# use), with the real zip payload encrypted inside an internal
|
|
# stream. 'unzip -t' correctly fails on that - not because
|
|
# anything is corrupt, but because it is legitimately not a zip
|
|
# file. Recognize this case before treating a failed zip check
|
|
# as corruption.
|
|
local _head8o
|
|
_head8o="$(head -c8 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
if [[ "$_head8o" = "d0cf11e0a1b11ae1" ]] ; then
|
|
echo "VALID|OLE2/CFBF container signature ok - this is a password-protected Office file (encrypted package), not a plain zip, so the zip check does not apply; open it with the password to verify content"
|
|
elif command -v unzip > /dev/null 2>&1 ; then
|
|
# Use a 120-second timeout: unzip -tq on a very large or partially
|
|
# corrupt ZIP can block indefinitely otherwise (the process simply
|
|
# stalls in I/O, unlike a clean CRC error which exits quickly).
|
|
local _unzip_exit
|
|
timeout 120 unzip -tq "$_f" > /dev/null 2>&1
|
|
_unzip_exit=$?
|
|
if [[ $_unzip_exit -eq 0 ]] ; then
|
|
echo "VALID|zip integrity ok"
|
|
elif [[ $_unzip_exit -eq 124 ]] ; then
|
|
echo "UNVERIFIED|zip integrity check timed out after 120 s (file may be very large or corrupt; check manually with: unzip -t \"$_f\")"
|
|
else
|
|
local _badentry
|
|
_badentry="$(trim "$(timeout 120 unzip -t "$_f" 2>&1 | grep -v '^Archive:' | grep -v '^[[:space:]]*$' | head -1)")"
|
|
echo "INVALID|zip integrity check failed${_badentry:+ (${_badentry})}"
|
|
fi
|
|
else
|
|
echo "UNVERIFIED|unzip not installed"
|
|
fi
|
|
;;
|
|
mp4|mov|m4v)
|
|
if head -c 64 "$_f" 2> /dev/null | grep -aq "ftyp" ; then
|
|
echo "VALID|mp4 ftyp box found"
|
|
else
|
|
echo "INVALID|mp4 ftyp box not found"
|
|
fi
|
|
;;
|
|
doc|xls|ppt|ole|msi)
|
|
local _head8
|
|
_head8="$(head -c8 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
if [[ "$_head8" = "d0cf11e0a1b11ae1" ]] ; then
|
|
echo "VALID|OLE2 Compound File signature ok (legacy Office)"
|
|
else
|
|
echo "INVALID|OLE2 Compound File signature missing"
|
|
fi
|
|
;;
|
|
azw3|mobi|prc)
|
|
# PalmDOC/Mobipocket container: 8-byte type/creator id
|
|
# 'BOOKMOBI' at fixed offset 60 in the header.
|
|
local _sig
|
|
_sig="$(dd if="$_f" bs=1 skip=60 count=8 2> /dev/null)"
|
|
if [[ "$_sig" = "BOOKMOBI" ]] ; then
|
|
echo "VALID|BOOKMOBI (Kindle/Mobi) signature ok"
|
|
else
|
|
echo "INVALID|BOOKMOBI signature missing at offset 60"
|
|
fi
|
|
;;
|
|
ai)
|
|
# Modern Illustrator files are PDF-compatible by default;
|
|
# older ones are plain PostScript/EPS.
|
|
if [[ "$(head -c5 "$_f" 2> /dev/null)" = "%PDF-" ]] ; then
|
|
if _pdf_check "$_f" ; then
|
|
echo "VALID|AI (PDF-compatible) - $_pdf_check_detail"
|
|
else
|
|
echo "UNVERIFIED|AI (PDF-compatible) but $_pdf_check_detail"
|
|
fi
|
|
elif head -c 12 "$_f" 2> /dev/null | grep -qa '^%!PS-Adobe' ; then
|
|
echo "VALID|AI (legacy PostScript/EPS) header ok"
|
|
else
|
|
echo "INVALID|neither PDF- nor %!PS-Adobe header found"
|
|
fi
|
|
;;
|
|
htm|html)
|
|
if head -c 512 "$_f" 2> /dev/null | tr '[:upper:]' '[:lower:]' | grep -qE '<html|<!doctype html' ; then
|
|
echo "VALID|HTML tag found near start"
|
|
else
|
|
echo "INVALID|no <html>/<!DOCTYPE html> found near start"
|
|
fi
|
|
;;
|
|
css)
|
|
# Plain text + balanced braces is a solid signal: decryption
|
|
# garbage is essentially never valid, balanced-brace text.
|
|
if LC_ALL=C grep -qP '[\x00-\x08\x0E-\x1F\x7F]' "$_f" 2> /dev/null ; then
|
|
echo "INVALID|contains binary/control bytes, not plausible CSS text"
|
|
else
|
|
local _open _close
|
|
_open=$(grep -o '{' "$_f" 2> /dev/null | wc -l)
|
|
_close=$(grep -o '}' "$_f" 2> /dev/null | wc -l)
|
|
if [[ "$_open" -gt 0 && "$_open" -eq "$_close" ]] ; then
|
|
echo "VALID|plain text, ${_open} matching { } pairs"
|
|
else
|
|
echo "INVALID|plain text but { (${_open}) / } (${_close}) counts don't match"
|
|
fi
|
|
fi
|
|
;;
|
|
ttf|otf)
|
|
local _head4f
|
|
_head4f="$(head -c4 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
# 00010000 = TrueType, 4F54544F = 'OTTO' (OpenType/CFF),
|
|
# 74727565 = 'true' (old Mac TrueType), 74746366 = 'ttcf' (collection)
|
|
if [[ "$_head4f" = "00010000" || "$_head4f" = "4f54544f" || "$_head4f" = "74727565" || "$_head4f" = "74746366" ]] ; then
|
|
echo "VALID|TrueType/OpenType font signature ok"
|
|
else
|
|
echo "INVALID|TrueType/OpenType font signature missing (head=$_head4f)"
|
|
fi
|
|
;;
|
|
woff)
|
|
if [[ "$(head -c4 "$_f" 2> /dev/null)" = "wOFF" ]] ; then
|
|
echo "VALID|WOFF font signature ok"
|
|
else
|
|
echo "INVALID|WOFF font signature missing"
|
|
fi
|
|
;;
|
|
woff2)
|
|
if [[ "$(head -c4 "$_f" 2> /dev/null)" = "wOF2" ]] ; then
|
|
echo "VALID|WOFF2 font signature ok"
|
|
else
|
|
echo "INVALID|WOFF2 font signature missing"
|
|
fi
|
|
;;
|
|
psd)
|
|
if [[ "$(head -c4 "$_f" 2> /dev/null)" = "8BPS" ]] ; then
|
|
echo "VALID|Photoshop (8BPS) signature ok"
|
|
else
|
|
echo "INVALID|Photoshop (8BPS) signature missing"
|
|
fi
|
|
;;
|
|
xcf)
|
|
if head -c 9 "$_f" 2> /dev/null | grep -qa '^gimp xcf' ; then
|
|
echo "VALID|GIMP (gimp xcf) signature ok"
|
|
else
|
|
echo "INVALID|GIMP (gimp xcf) signature missing"
|
|
fi
|
|
;;
|
|
wav)
|
|
local _riff _wave
|
|
_riff="$(head -c4 "$_f" 2> /dev/null)"
|
|
_wave="$(dd if="$_f" bs=1 skip=8 count=4 2> /dev/null)"
|
|
if [[ "$_riff" = "RIFF" && "$_wave" = "WAVE" ]] ; then
|
|
echo "VALID|RIFF/WAVE signature ok"
|
|
else
|
|
echo "INVALID|RIFF/WAVE signature missing"
|
|
fi
|
|
;;
|
|
ico)
|
|
local _head4i
|
|
_head4i="$(head -c4 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
if [[ "$_head4i" = "00000100" ]] ; then
|
|
echo "VALID|ICO signature ok"
|
|
else
|
|
echo "INVALID|ICO signature missing (head=$_head4i)"
|
|
fi
|
|
;;
|
|
rtf)
|
|
if head -c 6 "$_f" 2> /dev/null | grep -qa '^{\\rtf' ; then
|
|
echo "VALID|RTF signature ok"
|
|
else
|
|
echo "INVALID|RTF signature missing"
|
|
fi
|
|
;;
|
|
exe|dll|sys|scr|ocx|cpl)
|
|
# Windows PE: 'MZ' at offset 0, then at the offset stored (as a
|
|
# little-endian uint32) at 0x3C there must be a 'PE\0\0' signature.
|
|
local _mz _pe_off_hex _pe_off _pe_sig
|
|
_mz="$(head -c2 "$_f" 2> /dev/null)"
|
|
if [[ "$_mz" != "MZ" ]] ; then
|
|
echo "INVALID|PE 'MZ' header missing"
|
|
else
|
|
_pe_off_hex="$(dd if="$_f" bs=1 skip=60 count=4 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
# little-endian: reverse the byte order before interpreting as hex
|
|
_pe_off=$((16#${_pe_off_hex:6:2}${_pe_off_hex:4:2}${_pe_off_hex:2:2}${_pe_off_hex:0:2}))
|
|
_pe_sig="$(dd if="$_f" bs=1 skip="$_pe_off" count=4 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
if [[ "$_pe_sig" = "50450000" ]] ; then
|
|
echo "VALID|PE ('MZ' + 'PE\\0\\0') signature ok"
|
|
else
|
|
echo "INVALID|'MZ' header ok but 'PE' signature missing at offset $_pe_off (got $_pe_sig)"
|
|
fi
|
|
fi
|
|
;;
|
|
cab)
|
|
if [[ "$(head -c4 "$_f" 2> /dev/null)" = "MSCF" ]] ; then
|
|
echo "VALID|Microsoft Cabinet (MSCF) signature ok"
|
|
else
|
|
echo "INVALID|Microsoft Cabinet (MSCF) signature missing"
|
|
fi
|
|
;;
|
|
js|mjs|json|xml|svg|txt|csv|tsv|ini|cfg|conf|log|md|yml|yaml|properties|sql|sh|bash|py|php|java|c|h|cpp|hpp)
|
|
# Plain-text formats: broken decryption output is essentially
|
|
# never clean, printable text over any non-trivial length, so
|
|
# "no binary/control bytes" is itself strong positive evidence -
|
|
# much stronger than for an unknown/binary extension, where the
|
|
# same check only feeds the generic fallback below.
|
|
if LC_ALL=C grep -qP '[\x00-\x08\x0E-\x1F\x7F]' "$_f" 2> /dev/null ; then
|
|
echo "INVALID|contains binary/control bytes, not plausible '.${_ext}' text"
|
|
else
|
|
echo "VALID|plain text, no binary/control bytes (no deeper '.${_ext}' syntax check performed)"
|
|
fi
|
|
;;
|
|
indd|indt|indl)
|
|
# Adobe InDesign document/template/library container. There is
|
|
# no open decoder to fully parse it, but every valid file (since
|
|
# CS2/CS4) starts with this fixed 16-byte GUID-like signature -
|
|
# a corrupted/garbage decryption result will essentially never
|
|
# reproduce it by chance.
|
|
local _head16
|
|
_head16="$(head -c16 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
if [[ "$_head16" = "0606edf5d81d46e5bd31efe7fe74b71d" ]] ; then
|
|
echo "VALID|InDesign container signature ok (content/layout itself not parsed - open in InDesign to fully confirm)"
|
|
else
|
|
echo "INVALID|InDesign container signature missing (head=$_head16)"
|
|
fi
|
|
;;
|
|
*)
|
|
_validate_by_content_or_heuristic "$_f" "$_ext" "$_size" "$_origsize"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
## - PDF structural check shared by the 'pdf' case and the 'ai'
|
|
## - (PDF-compatible) case. Sets $_pdf_check_detail, returns 0/1.
|
|
## -
|
|
_pdf_check() {
|
|
local _f="$1"
|
|
if [[ "$(head -c5 "$_f" 2> /dev/null)" != "%PDF-" ]] ; then
|
|
_pdf_check_detail="missing %PDF- header"
|
|
return 1
|
|
fi
|
|
if command -v qpdf > /dev/null 2>&1 ; then
|
|
if qpdf --check "$_f" > /dev/null 2>&1 ; then
|
|
_pdf_check_detail="qpdf --check ok"
|
|
return 0
|
|
else
|
|
_pdf_check_detail="qpdf --check failed"
|
|
return 1
|
|
fi
|
|
fi
|
|
_pdf_check_detail="PDF header ok (qpdf not installed, no deep check)"
|
|
return 0
|
|
}
|
|
|
|
## - JPEG structural check - see recover_bad_signature.sh for the full
|
|
## - rationale (a raw "ends in FFD9" byte check produces false
|
|
## - positives; prefer an actual decoder). Sets $_jpeg_check_detail,
|
|
## - returns 0/1.
|
|
_jpeg_check() {
|
|
local _f="$1"
|
|
if command -v identify > /dev/null 2>&1 ; then
|
|
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
|
|
_jpeg_check_detail="ImageMagick 'identify' decoded the image ok"
|
|
return 0
|
|
else
|
|
_jpeg_check_detail="ImageMagick 'identify' failed to decode the image"
|
|
return 1
|
|
fi
|
|
fi
|
|
if command -v php > /dev/null 2>&1 ; then
|
|
local _phpout
|
|
_phpout="$(php -r '
|
|
error_reporting(0);
|
|
if (!function_exists("imagecreatefromjpeg")) { echo "NOGD"; exit(0); }
|
|
$im = @imagecreatefromjpeg($argv[1]);
|
|
if ($im === false) { echo "BAD"; exit(0); }
|
|
imagedestroy($im);
|
|
echo "OK";
|
|
' "$_f" 2> /dev/null)"
|
|
case "$_phpout" in
|
|
OK)
|
|
_jpeg_check_detail="PHP GD (imagecreatefromjpeg) decoded the image ok"
|
|
return 0
|
|
;;
|
|
BAD)
|
|
_jpeg_check_detail="PHP GD (imagecreatefromjpeg) failed to decode the image"
|
|
return 1
|
|
;;
|
|
esac
|
|
# NOGD or empty output: GD extension not available, fall through
|
|
fi
|
|
# last resort: the old, less reliable marker heuristic
|
|
local _head _tail
|
|
_head="$(head -c3 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
_tail="$(tail -c2 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
|
|
if [[ "$_head" = "ffd8ff" && "$_tail" = "ffd9" ]] ; then
|
|
_jpeg_check_detail="JPEG SOI/EOI markers ok (no decoder available for a deep check)"
|
|
return 0
|
|
elif [[ "$_head" = "ffd8ff" ]] ; then
|
|
_jpeg_check_detail="JPEG SOI marker ok but no decoder available for a deep check; raw EOI-at-EOF heuristic failed (head=$_head tail=$_tail) - this is NOT reliable, many valid JPEGs carry trailer bytes after EOI, verify manually or install ImageMagick/php-gd"
|
|
return 1
|
|
else
|
|
_jpeg_check_detail="JPEG SOI marker missing (head=$_head)"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
## - Fallback for extensions without a dedicated structural check (or
|
|
## - no extension at all) - see recover_bad_signature.sh for the full
|
|
## - rationale.
|
|
## -
|
|
_validate_by_content_or_heuristic() {
|
|
local _f="$1" _ext="$2" _size="$3" _origsize="$4"
|
|
local _magic=""
|
|
|
|
if command -v file > /dev/null 2>&1 ; then
|
|
_magic="$(file --mime-type -b "$_f" 2> /dev/null)"
|
|
fi
|
|
|
|
case "$_magic" in
|
|
application/pdf)
|
|
if _pdf_check "$_f" ; then
|
|
echo "VALID|content-detected PDF ('.${_ext:-<none>}' extension), $_pdf_check_detail"
|
|
else
|
|
echo "UNVERIFIED|content-detected PDF ('.${_ext:-<none>}' extension), but $_pdf_check_detail"
|
|
fi
|
|
return
|
|
;;
|
|
image/jpeg|image/png|image/gif|image/bmp|image/tiff|application/zip|video/mp4|video/quicktime)
|
|
echo "UNVERIFIED|content-detected as $_magic (extension was '.${_ext:-<none>}') - re-run with that extension to structurally validate, or check manually"
|
|
return
|
|
;;
|
|
esac
|
|
|
|
local _looks_like_text="unknown"
|
|
if LC_ALL=C grep -qP '[\x00-\x08\x0E-\x1F\x7F]' "$_f" 2> /dev/null ; then
|
|
_looks_like_text="no (contains binary/control bytes)"
|
|
else
|
|
_looks_like_text="yes (no binary/control bytes found)"
|
|
fi
|
|
|
|
local _size_note
|
|
if [[ -n "$_origsize" && "$_origsize" =~ ^[0-9]+$ && "$_origsize" -gt 0 && "$_size" -gt 0 ]] ; then
|
|
local _ratio_pct=$(( _size * 100 / _origsize ))
|
|
if [[ "$_ratio_pct" -ge 65 && "$_ratio_pct" -le 95 ]] ; then
|
|
_size_note="size ratio plausible (recovered ${_size} / original ${_origsize} = ${_ratio_pct}%, expected ~74-86% for this instance)"
|
|
else
|
|
_size_note="size ratio LOOKS OFF (recovered ${_size} / original ${_origsize} = ${_ratio_pct}%, expected ~74-86% - check manually)"
|
|
fi
|
|
else
|
|
_size_note="size comparison not possible (recovered ${_size} bytes, original ${_origsize:-?} bytes)"
|
|
fi
|
|
|
|
echo "UNVERIFIED|no dedicated check for '.${_ext:-<none>}', detected type: ${_magic:-file/libmagic not installed}, looks like text: ${_looks_like_text}, ${_size_note}"
|
|
}
|
|
|
|
blank_line() {
|
|
if $terminal ; then
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
# - Running in a terminal?
|
|
# -
|
|
if [[ -t 1 ]] ; then
|
|
terminal=true
|
|
else
|
|
terminal=false
|
|
fi
|
|
|
|
# - This script needs root privileges (reads other users' files under
|
|
# - the Nextcloud data directory for the raw ciphertext backup, 'su'
|
|
# - into the webserver user to perform the actual restore writes).
|
|
# -
|
|
if [[ "$(id -u)" -ne 0 ]] ; then
|
|
fatal "This script must be run as root (it needs to read the Nextcloud data directory for backups and 'su' into the webserver user). Please re-run as root, e.g. via sudo."
|
|
fi
|
|
|
|
# ----------
|
|
# - Jobhandling
|
|
# ----------
|
|
|
|
if pgrep -f "$(basename $0)" | grep -q -v $$ ; then
|
|
|
|
msg="A previos instance of script \"`basename $0`\" seems already be running."
|
|
|
|
echo ""
|
|
if $terminal ; then
|
|
echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg"
|
|
echo ""
|
|
echo -e " \033[31m\033[1mScript was interupted\033[m!"
|
|
else
|
|
echo " [ Fatal ]: $msg"
|
|
echo ""
|
|
echo " Script was interupted!"
|
|
fi
|
|
echo
|
|
|
|
exit 1
|
|
else
|
|
if [[ -d "$LOCK_DIR" ]] ; then
|
|
rm -rf "$LOCK_DIR" 2> /dev/null
|
|
fi
|
|
fi
|
|
|
|
|
|
# - If job already runs, stop execution..
|
|
# -
|
|
if mkdir "$LOCK_DIR" 2> /dev/null ; then
|
|
|
|
# - Remove lockdir when the script finishes, or when it receives a signal
|
|
# -
|
|
trap clean_up SIGHUP SIGINT SIGTERM
|
|
|
|
else
|
|
|
|
msg="A previos instance of script \"`basename $0`\" seems already be running."
|
|
|
|
echo ""
|
|
if $terminal ; then
|
|
echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg"
|
|
echo ""
|
|
echo -e " \033[31m\033[1mScript was interupted\033[m!"
|
|
else
|
|
echo " [ Fatal ]: $msg"
|
|
echo ""
|
|
echo " Script was interupted!"
|
|
fi
|
|
echo
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
# -------------
|
|
# - Read in Commandline arguments
|
|
# -------------
|
|
DRY_RUN=false
|
|
_dry_run_explicit=false
|
|
|
|
while getopts hns: opt ; do
|
|
case $opt in
|
|
h) usage ;;
|
|
n) DRY_RUN=true; _dry_run_explicit=true ;;
|
|
s) WEBSITE=$OPTARG ;;
|
|
\?) usage
|
|
esac
|
|
done
|
|
|
|
|
|
# =============
|
|
# --- Ask which mode to run in, unless '-n' was already given on the
|
|
# --- command line (that still works unchanged for scripted/non-
|
|
# --- interactive invocations). Unlike recover_bad_signature.sh, the
|
|
# --- SAFE choice (dry-run) is the default here on purpose - this
|
|
# --- script writes into live Nextcloud storage.
|
|
# =============
|
|
if ! $_dry_run_explicit && $terminal ; then
|
|
|
|
blank_line
|
|
echo -e "\033[37m\033[1mWhich mode should this run use?\033[m"
|
|
echo ""
|
|
echo -e " \033[1m[1] Dry-run\033[m - go through everything (selection, re-validation, reporting), but write, back up and verify NOTHING"
|
|
echo " [2] Real restore - actually back up, overwrite and verify each file in Nextcloud"
|
|
info "Just press Return to use the default: [1] Dry-run."
|
|
echo -n " Select mode by number [1]: "
|
|
read _mode_choice
|
|
|
|
case "$(trim "$_mode_choice")" in
|
|
2) DRY_RUN=false ;;
|
|
""|1) DRY_RUN=true ;;
|
|
*) fatal "Invalid selection '$_mode_choice'." ;;
|
|
esac
|
|
|
|
fi
|
|
|
|
|
|
if [[ -z "$WEBSITE" ]] ; then
|
|
|
|
while IFS='' read -r -d '' _conf_file ; do
|
|
source $_conf_file
|
|
if [[ -n "$WEBSITE" ]] ; then
|
|
unsorted_website_arr+=("${WEBSITE}:$_conf_file")
|
|
fi
|
|
WEBSITE=""
|
|
done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0)
|
|
|
|
# - Sort array
|
|
# -
|
|
IFS=$'\n' website_arr=($(sort <<<"${unsorted_website_arr[*]}"))
|
|
|
|
# Which cloud instance (website) would you like to update
|
|
#
|
|
source ${snippet_dir}/get-cloud-instance-to-update.sh
|
|
|
|
else
|
|
|
|
while IFS='' read -r -d '' _conf_file ; do
|
|
if $(grep -E -q "WEBSITE=\"?${WEBSITE}\"?" ${_conf_file} 2> /dev/null) ; then
|
|
conf_file="${_conf_file}"
|
|
break
|
|
fi
|
|
done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0)
|
|
|
|
fi
|
|
|
|
|
|
# - Reset IFS
|
|
# -
|
|
IFS=$CUR_IFS
|
|
|
|
DEFAULT_SRC_BASE_DIR="/usr/local/src/nextcloud"
|
|
DEFAULT_HTTP_USER="www-data"
|
|
DEFAULT_HTTP_GROUP="www-data"
|
|
DEFAULT_PHP_ENGINE='FPM'
|
|
|
|
blank_line
|
|
echononl " Include Configuration file '$(basename "${conf_file}")'.."
|
|
if [[ ! -f $conf_file ]]; then
|
|
echo_skipped
|
|
fatal "Missing configuration file '$conf_file'."
|
|
else
|
|
source $conf_file
|
|
echo_ok
|
|
fi
|
|
|
|
DEFAULT_WEB_BASE_DIR="/var/www/${WEBSITE}"
|
|
[[ -n "$WEB_BASE_DIR" ]] || WEB_BASE_DIR=$DEFAULT_WEB_BASE_DIR
|
|
|
|
if [[ ! -d ${WEB_BASE_DIR} ]] ; then
|
|
fatal "Web base directory '$WEB_BASE_DIR' not found!"
|
|
fi
|
|
|
|
DATA_DIR="$(realpath ${WEB_BASE_DIR}/data)"
|
|
|
|
[[ -n "$PHP_ENGINE" ]] || PHP_ENGINE=$DEFAULT_PHP_ENGINE
|
|
|
|
INSTALL_DIR="$(realpath ${WEB_BASE_DIR}/nextcloud)"
|
|
CURRENT_VERSION="$(basename $INSTALL_DIR | cut -d"-" -f2)"
|
|
|
|
# - Muss auf denselben Ort zeigen wie beim recover_bad_signature.sh-Lauf,
|
|
# - der die hier verwendeten Dateien erzeugt hat.
|
|
# -
|
|
[[ -n "$RECOVERY_BASE_DIR" ]] || RECOVERY_BASE_DIR=$DEFAULT_RECOVERY_BASE_DIR
|
|
recovery_dir="${RECOVERY_BASE_DIR}/${WEBSITE}"
|
|
|
|
[[ -n "$RESTORE_BACKUP_BASE_DIR" ]] || RESTORE_BACKUP_BASE_DIR=$DEFAULT_RESTORE_BACKUP_BASE_DIR
|
|
backup_dir="${RESTORE_BACKUP_BASE_DIR}/${WEBSITE}"
|
|
|
|
|
|
# =============
|
|
# --- Some
|
|
# =============
|
|
|
|
# - Support systemd ?
|
|
# -
|
|
SYSTEMD_EXISTS=false
|
|
systemd=$(which systemd)
|
|
systemctl=$(which systemctl)
|
|
|
|
if [[ -n "$systemd" ]] || [[ -n "$systemctl" ]] ; then
|
|
SYSTEMD_EXISTS=true
|
|
fi
|
|
|
|
if $terminal ; then
|
|
echo ""
|
|
echo -e "\033[32m-----\033[m"
|
|
echo -e "Restore already-recovered \033[1mBad Signature\033[m files back into system \033[1m${WEB_BASE_DIR}\033[m"
|
|
echo -e "\033[1m
|
|
This WRITES into Nextcloud's live storage. Only files marked VALID
|
|
or plausible/UNVERIFIED ('Nicht pruefbar', but nothing looks wrong)
|
|
by a prior recover_bad_signature.sh run are ever restored, each is
|
|
re-validated again right before writing, the current (still broken)
|
|
original is backed up first, and every write is verified by reading
|
|
it back afterwards.\033[m"
|
|
echo -e "\033[32m-----\033[m"
|
|
fi
|
|
|
|
|
|
# =============
|
|
# --- Some checks
|
|
# =============
|
|
|
|
DEFAULT_HTTP_USER="www-data"
|
|
DEFAULT_HTTP_GROUP="www-data"
|
|
|
|
|
|
NGINX_IS_ENABLED=false
|
|
APACHE2_IS_ENABLED=false
|
|
|
|
# Get Webservice environment as IS_HTTPD_RUNNING, HTTP_USER, HTTP_GROUP..
|
|
#
|
|
source ${snippet_dir}/get-webservice-environment.sh
|
|
|
|
|
|
# Check PHP Version
|
|
#
|
|
source ${snippet_dir}/get-php-major-version.sh
|
|
|
|
|
|
# Get full qualified PHP command
|
|
#
|
|
source ${snippet_dir}/get-path-of-php-command.sh
|
|
|
|
|
|
if [[ ! -x "$PHP_BIN" ]]; then
|
|
fatal "No PHP binary found!"
|
|
fi
|
|
|
|
|
|
# =============
|
|
# --- Check Server-Side Encryption status (informational - this script
|
|
# --- works either way, but the whole point of it assumes SSE is on)
|
|
# =============
|
|
|
|
blank_line
|
|
echononl " Check Server-Side Encryption status.."
|
|
|
|
_encryption_status_out="$(su -c "$PHP_BIN $INSTALL_DIR/occ encryption:status" -s /bin/bash $HTTP_USER 2> "$log_file")"
|
|
|
|
ENCRYPTION_ENABLED="unknown"
|
|
|
|
if [[ -s "$log_file" ]] ; then
|
|
echo_failed
|
|
error "$(cat "$log_file")"
|
|
elif echo "$_encryption_status_out" | grep -qiE 'enabled:[[:space:]]*true' ; then
|
|
echo_ok
|
|
ENCRYPTION_ENABLED="yes"
|
|
else
|
|
echo_warning
|
|
ENCRYPTION_ENABLED="no"
|
|
warn "Server-Side Encryption appears to be disabled on ${WEBSITE} right now. Restoring will still work (files are simply written unencrypted), but double-check this is expected."
|
|
fi
|
|
|
|
|
|
# =============
|
|
# --- Determine available recovery reports (output of
|
|
# --- recover_bad_signature.sh - NOT the bad_signature_*.tsv scan
|
|
# --- reports)
|
|
# =============
|
|
|
|
mapfile -t _available_reports < <(ls -t "${report_dir}"/recovery_*.tsv 2> /dev/null)
|
|
|
|
if [[ ${#_available_reports[@]} -eq 0 ]] ; then
|
|
fatal "No recovery report files (recovery_*.tsv) found in '${report_dir}'. Run recover_bad_signature.sh first."
|
|
fi
|
|
|
|
blank_line
|
|
if $terminal ; then
|
|
echo -e "\033[37m\033[1mAvailable recovery reports (newest first)\033[m"
|
|
echo ""
|
|
_i=1
|
|
for _f in "${_available_reports[@]}" ; do
|
|
printf " [%2d] %s\n" "$_i" "$(basename "$_f")"
|
|
(( _i++ ))
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
echo -n " Select report by number: "
|
|
read _report_choice
|
|
|
|
if ! [[ "$_report_choice" =~ ^[0-9]+$ ]] || [[ "$_report_choice" -lt 1 ]] || [[ "$_report_choice" -gt ${#_available_reports[@]} ]] ; then
|
|
fatal "Invalid selection '$_report_choice'."
|
|
fi
|
|
|
|
chosen_report="${_available_reports[$((_report_choice-1))]}"
|
|
|
|
|
|
# =============
|
|
# --- Extract "user<TAB>path<TAB>original_size<TAB>validation" quadruples
|
|
# --- for every restorable entry in the chosen report. The report has no
|
|
# --- per-row user column - accounts are section headers ('---------' /
|
|
# --- '<user>' / '---------'). Only 6-tab-field data rows belong to the
|
|
# --- following case; the header row, the 'Datenmuell'/'Nicht pruefbar'
|
|
# --- detail blocks (2 fields) and the account-statistics lines (no
|
|
# --- tabs) never match NF==6.
|
|
# ---
|
|
# --- Two validation values are picked up, both restored by default:
|
|
# --- VALID - a dedicated structural check confirmed the file.
|
|
# --- UNVERIFIED - no dedicated check exists ('Nicht pruefbar' in the
|
|
# --- recovery report), but nothing about it looks
|
|
# --- actively wrong either. The one sub-case that IS a
|
|
# --- concrete negative signal - the detail text says
|
|
# --- the recovered/original size ratio 'LOOKS OFF' -
|
|
# --- is deliberately excluded here, same treatment as
|
|
# --- INVALID/'Datenmuell': not restored automatically.
|
|
# --- original_size ($3) is carried through so this same distinction
|
|
# --- (plausible size ratio or not) can be re-checked with real data
|
|
# --- right before writing, not just trusted from the old report.
|
|
# =============
|
|
|
|
valid_entries_file="${LOCK_DIR}/valid_entries.tsv"
|
|
awk -F'\t' '
|
|
/^-+$/ {
|
|
if (state == 0) { state = 1 }
|
|
else if (state == 2) { state = 0 }
|
|
next
|
|
}
|
|
{
|
|
if (state == 1) { user = $0; state = 2; next }
|
|
if (NF == 6 && $1 != "path" && ($5 == "VALID" || ($5 == "UNVERIFIED" && $6 !~ /LOOKS OFF/))) {
|
|
print user "\t" $1 "\t" $3 "\t" $5
|
|
}
|
|
}
|
|
' "$chosen_report" > "$valid_entries_file"
|
|
|
|
unsorted_account_arr=($(cut -f1 "$valid_entries_file" | sort -u))
|
|
IFS=$'\n' account_arr=($(sort <<<"${unsorted_account_arr[*]}"))
|
|
IFS=$CUR_IFS
|
|
|
|
if [[ ${#account_arr[@]} -eq 0 ]] ; then
|
|
fatal "No restorable (VALID or plausible UNVERIFIED) entries found in '$(basename "$chosen_report")' - nothing to restore."
|
|
fi
|
|
|
|
blank_line
|
|
if $terminal ; then
|
|
echo -e "\033[37m\033[1mAccounts with restorable (VALID + plausible UNVERIFIED) entries in this report\033[m"
|
|
echo ""
|
|
for _a in "${account_arr[@]}" ; do
|
|
_a_count=$(awk -F'\t' -v u="$_a" '$1==u' "$valid_entries_file" | wc -l)
|
|
_a_count_unverified=$(awk -F'\t' -v u="$_a" '$1==u && $4=="UNVERIFIED"' "$valid_entries_file" | wc -l)
|
|
if [[ "$_a_count_unverified" -gt 0 ]] ; then
|
|
echo " - $_a (${_a_count}, davon ${_a_count_unverified} 'Nicht pruefbar'/plausibel)"
|
|
else
|
|
echo " - $_a (${_a_count})"
|
|
fi
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
echo -n " Account(s) to restore VALID/plausible-UNVERIFIED files for (blank separated, or 'all'): "
|
|
read _input
|
|
|
|
while true ; do
|
|
|
|
IFS=' ' read -r -a unsorted_selected_user_arr <<< "$(trim "$_input")"
|
|
|
|
if [[ "${#unsorted_selected_user_arr[@]}" -eq 1 && "${unsorted_selected_user_arr[0],,}" = "all" ]] ; then
|
|
selected_user_arr=("${account_arr[@]}")
|
|
break
|
|
fi
|
|
|
|
_all_valid=true
|
|
for _u in "${unsorted_selected_user_arr[@]}" ; do
|
|
if ! containsElement "$_u" "${account_arr[@]}" ; then
|
|
error "Unknown account '$_u' (not found in the selected report)."
|
|
_all_valid=false
|
|
fi
|
|
done
|
|
|
|
if $_all_valid && [[ "${#unsorted_selected_user_arr[@]}" -gt 0 ]] ; then
|
|
IFS=$'\n' selected_user_arr=($(sort <<<"${unsorted_selected_user_arr[*]}"))
|
|
IFS=$CUR_IFS
|
|
break
|
|
fi
|
|
|
|
echo -n " Account(s) to restore VALID/plausible-UNVERIFIED files for (blank separated, or 'all'): "
|
|
read _input
|
|
|
|
done
|
|
|
|
|
|
# =============
|
|
# --- Confirmation
|
|
# =============
|
|
|
|
mkdir -p "$report_dir" 2> /dev/null
|
|
restore_report_file="${report_dir}/restore_${WEBSITE}_${run_date}.tsv"
|
|
|
|
if $terminal ; then
|
|
echo ""
|
|
if $DRY_RUN ; then
|
|
echo -e "\033[1;32mStarting DRY-RUN restore for \033[1;37m${WEBSITE}\033[m"
|
|
else
|
|
echo -e "\033[1;31m\033[1mStarting REAL restore (writes into live Nextcloud storage) for \033[1;37m${WEBSITE}\033[m"
|
|
fi
|
|
echo ""
|
|
echo -e " Cloud instance........................: $WEBSITE"
|
|
echo ""
|
|
echo -e " Recovery report used...................: $(basename "$chosen_report")"
|
|
echo -e " Recovered files read from..............: $recovery_dir"
|
|
echo -e " Account(s) to restore..................: \033[33m${selected_user_arr[*]}\033[m"
|
|
echo ""
|
|
if $DRY_RUN ; then
|
|
echo -e " Nothing will be written - dry-run only."
|
|
else
|
|
echo -e " Ciphertext backup (before overwrite)...: $backup_dir"
|
|
echo -e " Files written back to their ORIGINAL path in Nextcloud."
|
|
fi
|
|
echo -e " Restore report..........................: reports/$(basename "${restore_report_file}")"
|
|
echo ""
|
|
|
|
if $DRY_RUN ; then
|
|
info "Dry-run: selection and re-validation run for real, nothing is backed up, written or verified."
|
|
else
|
|
warn "This WRITES into Nextcloud's live storage, overwriting the still-broken original at its ORIGINAL path with the recovered content - the first script in this toolkit that does so. Only files re-validated as VALID, or as plausible UNVERIFIED (no dedicated check, but nothing looks wrong - clearly marked as such in the report), right now are touched. The current (still broken) original is copied byte-for-byte to '${backup_dir}' first, and every write is read back and verified afterwards, but please still spot-check a few results yourself - especially the UNVERIFIED ones."
|
|
fi
|
|
|
|
echo ""
|
|
echo -n " Type upper case 'YES' to continue executing with this parameters: "
|
|
read OK
|
|
if [[ "$OK" = "YES" ]] ; then
|
|
echo ""
|
|
echo ""
|
|
echo -e "\033[1;32mGoing to restore VALID/plausible-UNVERIFIED files for each selected account on \033[1;37m$WEBSITE \033[m"
|
|
else
|
|
fatal "Abort by user request - Answer as not 'YES'"
|
|
fi
|
|
fi
|
|
|
|
|
|
{
|
|
echo "=================================================================="
|
|
echo " Bad-Signature-Restore-Versuch ${WEBSITE} ${run_date}"
|
|
echo "=================================================================="
|
|
echo ""
|
|
echo " Basis-Report (Recovery): $(basename "$chosen_report")"
|
|
echo " Eintraege mit validation=VALID sowie validation=UNVERIFIED ('Nicht pruefbar',"
|
|
echo " aber Groessenverhaeltnis plausibel) aus diesem Report werden zurueckgeschrieben -"
|
|
echo " letztere sind in der Spalte 'origin' unten als UNVERIFIED gekennzeichnet."
|
|
$DRY_RUN && echo " DRY-RUN: es wurde NICHTS geschrieben, gesichert oder verifiziert."
|
|
! $DRY_RUN && echo " Chiffretext-Sicherung (vor dem Ueberschreiben) liegt unter: ${backup_dir}"
|
|
echo ""
|
|
echo -e "path\tbytes_written\tstatus\torigin\tdetail"
|
|
} > "$restore_report_file"
|
|
|
|
|
|
# =============
|
|
# --- Write the embedded PHP restore script
|
|
# =============
|
|
|
|
restore_php_file="${INSTALL_DIR}/.restore_bad_signature_$$.php"
|
|
|
|
cat > "$restore_php_file" <<'PHP_RESTORE_EOF'
|
|
<?php
|
|
/**
|
|
* restore_bad_signature.php
|
|
*
|
|
* Writes already-recovered, already re-validated plaintext files back
|
|
* into Nextcloud's own storage, at their ORIGINAL path, through the
|
|
* normal Files API - the same write path a regular upload/sync client
|
|
* write uses. Server-Side Encryption (if enabled) transparently
|
|
* (re-)encrypts the content with a fresh IV and a correct signature;
|
|
* 'encryption_skip_signature_check' is never touched by this script -
|
|
* a fresh write does not need it.
|
|
*
|
|
* After each write, the file is immediately read back through the
|
|
* SAME normal read path (no signature-check bypass) and compared
|
|
* byte-for-byte (SHA-256) against the source - proof the file is
|
|
* genuinely healed, not just that a write happened without error.
|
|
*
|
|
* Usage: php restore_bad_signature.php <uid> <listFile> [--dry-run]
|
|
* listFile: TSV with "path\tlocalRecoveredFile" per line (no header)
|
|
*/
|
|
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', '1');
|
|
|
|
register_shutdown_function(function () {
|
|
$err = error_get_last();
|
|
if ($err !== null && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
|
|
fwrite(STDERR, "DEBUG: FATAL bei Shutdown: [{$err['type']}] {$err['message']} in {$err['file']}:{$err['line']}\n");
|
|
}
|
|
});
|
|
|
|
define('OC_CONSOLE', 1);
|
|
|
|
$oldWorkingDir = getcwd();
|
|
if ($oldWorkingDir === false) {
|
|
fwrite(STDERR, "Konnte aktuelles Arbeitsverzeichnis nicht ermitteln. Bitte mit absolutem Pfad aufrufen.\n");
|
|
exit(1);
|
|
}
|
|
chdir(__DIR__);
|
|
require_once __DIR__ . '/lib/base.php';
|
|
// Nextcloud's own bootstrap (lib/base.php) unconditionally calls
|
|
// set_time_limit(3600) as part of the require above - this OVERRIDES
|
|
// whatever -d max_execution_time was passed on the PHP command line,
|
|
// since it is a later, explicit runtime call. Undo that here, now that
|
|
// the require is done, so restoring a large account isn't killed
|
|
// after exactly one hour regardless of the CLI flag.
|
|
set_time_limit(0);
|
|
chdir($oldWorkingDir);
|
|
|
|
if (function_exists('posix_getuid') && posix_getuid() === 0) {
|
|
fwrite(STDERR, "Bitte NICHT als root ausfuehren, sondern als Webserver-User.\n");
|
|
exit(1);
|
|
}
|
|
|
|
try {
|
|
$uid = $argv[1] ?? null;
|
|
$listFile = $argv[2] ?? null;
|
|
$dryRun = in_array('--dry-run', $argv, true);
|
|
|
|
if ($uid === null || $listFile === null) {
|
|
fwrite(STDERR, "Usage: php restore_bad_signature.php <uid> <listFile> [--dry-run]\n");
|
|
exit(1);
|
|
}
|
|
if (!is_file($listFile)) {
|
|
fwrite(STDERR, "Liste nicht gefunden: $listFile\n");
|
|
exit(1);
|
|
}
|
|
|
|
$rootFolder = \OC::$server->get(\OCP\Files\IRootFolder::class);
|
|
\OC_Util::setupFS($uid);
|
|
|
|
$lines = file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
if ($lines === false) {
|
|
$lines = [];
|
|
}
|
|
|
|
$total = 0;
|
|
$ok = 0;
|
|
$failed = 0;
|
|
|
|
echo "path\tbytes_written\tstatus\tdetail\n";
|
|
|
|
foreach ($lines as $line) {
|
|
$cols = explode("\t", $line);
|
|
$path = $cols[0] ?? '';
|
|
$local = $cols[1] ?? '';
|
|
if ($path === '' || $local === '') {
|
|
continue;
|
|
}
|
|
$total++;
|
|
|
|
if (!is_file($local) || !is_readable($local)) {
|
|
$failed++;
|
|
echo "$path\t0\tERROR\tlocal recovered file not readable: $local\n";
|
|
flush();
|
|
continue;
|
|
}
|
|
|
|
$localSize = filesize($local);
|
|
$localHash = hash_file('sha256', $local);
|
|
|
|
if ($dryRun) {
|
|
$ok++;
|
|
echo "$path\t$localSize\tDRY_RUN\twould write $localSize bytes and verify readback (sha256 $localHash)\n";
|
|
flush();
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$node = null;
|
|
try {
|
|
$node = $rootFolder->get($path);
|
|
} catch (\Throwable $e) {
|
|
$node = null;
|
|
}
|
|
|
|
if ($node !== null && !($node instanceof \OCP\Files\File)) {
|
|
$failed++;
|
|
echo "$path\t0\tERROR\ttarget exists but is not a regular file\n";
|
|
flush();
|
|
continue;
|
|
}
|
|
|
|
if ($node === null) {
|
|
// Original missing entirely (unexpected - the file this
|
|
// was recovered from should still be there, just
|
|
// unreadable). Create it fresh at the same path rather
|
|
// than silently skipping it.
|
|
$userFolder = $rootFolder->getUserFolder($uid);
|
|
$relative = preg_replace('#^/' . preg_quote($uid, '#') . '/files/#', '', $path);
|
|
if ($relative === null || $relative === '') {
|
|
$relative = ltrim($path, '/');
|
|
}
|
|
$node = $userFolder->newFile($relative);
|
|
}
|
|
|
|
$in = fopen($local, 'r');
|
|
if ($in === false) {
|
|
throw new \RuntimeException("lokale Datei konnte nicht geoeffnet werden: $local");
|
|
}
|
|
$node->putContent($in);
|
|
if (is_resource($in)) {
|
|
fclose($in);
|
|
}
|
|
|
|
// Verify: re-read through the SAME normal API path (no
|
|
// signature-check bypass) and compare byte-for-byte.
|
|
clearstatcache();
|
|
$freshNode = $rootFolder->get($path);
|
|
$readBack = $freshNode->fopen('r');
|
|
if ($readBack === false) {
|
|
$failed++;
|
|
echo "$path\t0\tWRITE_ERROR\tputContent() did not throw, but re-read (fopen) failed\n";
|
|
flush();
|
|
continue;
|
|
}
|
|
|
|
$ctx = hash_init('sha256');
|
|
$bytesRead = 0;
|
|
while (!feof($readBack)) {
|
|
$chunk = fread($readBack, 1048576);
|
|
if ($chunk === false) {
|
|
break;
|
|
}
|
|
$bytesRead += strlen($chunk);
|
|
hash_update($ctx, $chunk);
|
|
}
|
|
fclose($readBack);
|
|
$readBackHash = hash_final($ctx);
|
|
|
|
if ($readBackHash === $localHash && $bytesRead === $localSize) {
|
|
$ok++;
|
|
echo "$path\t$bytesRead\tOK\tverified: re-read $bytesRead bytes, sha256 matches source ($readBackHash)\n";
|
|
} else {
|
|
$failed++;
|
|
$hashNote = ($readBackHash === $localHash) ? 'sha256 matches' : 'sha256 MISMATCH';
|
|
echo "$path\t$bytesRead\tVERIFY_MISMATCH\tread back $bytesRead bytes (expected $localSize), $hashNote\n";
|
|
}
|
|
flush();
|
|
} catch (\Throwable $e) {
|
|
$failed++;
|
|
$msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage());
|
|
echo "$path\t0\tWRITE_ERROR\t" . get_class($e) . ": $msg\n";
|
|
flush();
|
|
}
|
|
}
|
|
|
|
fwrite(STDERR, "\nFertig. Dateien verarbeitet: $total, ok: $ok, Fehler: $failed\n");
|
|
} catch (\Throwable $e) {
|
|
fwrite(STDERR, "\n!!! UNBEHANDELTE AUSNAHME !!!\n");
|
|
fwrite(STDERR, get_class($e) . ": " . $e->getMessage() . "\n");
|
|
fwrite(STDERR, "in " . $e->getFile() . ":" . $e->getLine() . "\n");
|
|
fwrite(STDERR, $e->getTraceAsString() . "\n");
|
|
exit(2);
|
|
}
|
|
PHP_RESTORE_EOF
|
|
|
|
chmod 644 "$restore_php_file"
|
|
|
|
|
|
# -----
|
|
# - Main part of the script
|
|
# -----
|
|
|
|
if $terminal ; then
|
|
echo ""
|
|
echo ""
|
|
echo -e "\033[37m\033[1mMain part of the script\033[m"
|
|
echo ""
|
|
fi
|
|
|
|
if [[ ! -d "$recovery_dir" ]] ; then
|
|
fatal "Recovery directory '$recovery_dir' does not exist - run recover_bad_signature.sh first."
|
|
fi
|
|
|
|
if ! $DRY_RUN ; then
|
|
mkdir -p "$backup_dir" 2> /dev/null
|
|
fi
|
|
|
|
declare -i total_selected=0
|
|
declare -i total_selected_unverified=0
|
|
declare -i total_prevalidation_failed=0
|
|
declare -i total_ok=0
|
|
declare -i total_ok_unverified=0
|
|
declare -i total_write_errors=0
|
|
declare -i total_failed_users=0
|
|
|
|
for _user in "${selected_user_arr[@]}" ; do
|
|
|
|
_sep_len=${#_user}
|
|
[[ $_sep_len -lt 9 ]] && _sep_len=9
|
|
_sep_line="$(printf '%*s' "$_sep_len" '' | tr ' ' '-')"
|
|
|
|
{
|
|
echo ""
|
|
echo ""
|
|
echo "$_sep_line"
|
|
echo "$_user"
|
|
echo "$_sep_line"
|
|
} >> "$restore_report_file"
|
|
|
|
user_recovery_dir="${recovery_dir}/${_user}"
|
|
|
|
declare -i _user_selected=0
|
|
declare -i _user_selected_unverified=0
|
|
declare -i _user_prevalidation_failed=0
|
|
declare -a _user_prevalidation_failed_lines=()
|
|
declare -A _origin_of_path=()
|
|
|
|
list_file="${LOCK_DIR}/list_${_user}.tsv"
|
|
> "$list_file"
|
|
|
|
# =============
|
|
# --- Re-validate every VALID- or plausible-UNVERIFIED-marked entry
|
|
# --- against the CURRENT checks right before writing anything, and
|
|
# --- (unless dry-run) back up the current on-disk ciphertext
|
|
# --- byte-for-byte first. original_size (from the recovery report,
|
|
# --- $3 of valid_entries_file) is passed through so the size-ratio
|
|
# --- part of the UNVERIFIED check can run for real here too, not
|
|
# --- just be trusted from the old report.
|
|
# =============
|
|
while IFS=$'\t' read -r _u _path _origsize _recorded_class ; do
|
|
[[ "$_u" != "$_user" ]] && continue
|
|
(( _user_selected++ ))
|
|
|
|
_rel="${_path#/${_user}/files/}"
|
|
_local="${user_recovery_dir}/${_rel}"
|
|
|
|
if [[ ! -f "$_local" ]] ; then
|
|
(( _user_prevalidation_failed++ ))
|
|
_user_prevalidation_failed_lines+=("${_path}"$'\t'"recovered file missing under ${user_recovery_dir} - re-run recover_bad_signature.sh")
|
|
continue
|
|
fi
|
|
|
|
_val_result="$(validate_recovered_file "$_local" "$_path" "$_origsize")"
|
|
_val_class="${_val_result%%|*}"
|
|
_val_detail="${_val_result#*|}"
|
|
|
|
if [[ "$_val_class" == "VALID" ]] ; then
|
|
: # dedicated structural check confirmed it - restore as usual
|
|
elif [[ "$_val_class" == "UNVERIFIED" && "$_val_detail" != *"LOOKS OFF"* ]] ; then
|
|
(( _user_selected_unverified++ ))
|
|
else
|
|
(( _user_prevalidation_failed++ ))
|
|
_user_prevalidation_failed_lines+=("${_path}"$'\t'"no longer validates as restorable (${_val_class}: ${_val_detail}) - skipped, not restored")
|
|
continue
|
|
fi
|
|
|
|
if ! $DRY_RUN ; then
|
|
_orig_ciphertext="${DATA_DIR}/${_user}/files/${_rel}"
|
|
if [[ -f "$_orig_ciphertext" ]] ; then
|
|
_backup_target="${backup_dir}/${_user}/${_rel}"
|
|
mkdir -p "$(dirname "$_backup_target")" 2> /dev/null
|
|
cp -p "$_orig_ciphertext" "$_backup_target" 2> /dev/null
|
|
fi
|
|
fi
|
|
|
|
_origin_of_path["$_path"]="$_val_class"
|
|
printf '%s\t%s\n' "$_path" "$_local" >> "$list_file"
|
|
|
|
done < "$valid_entries_file"
|
|
|
|
if [[ $_user_selected -eq 0 ]] ; then
|
|
warn "No restorable (VALID or plausible UNVERIFIED) entries for account '${_user}' found in the selected report - skipping."
|
|
echo " [ keine VALID/UNVERIFIED-Eintraege im gewaehlten Report ]" >> "$restore_report_file"
|
|
continue
|
|
fi
|
|
|
|
user_result_tsv="${LOCK_DIR}/result_${_user}.tsv"
|
|
|
|
if $DRY_RUN ; then
|
|
echononl " Dry-run for account \033[1;37m${_user}\033[m (${_user_selected} selected, $(wc -l < "$list_file") to check).."
|
|
else
|
|
echononl " Restoring account \033[1;37m${_user}\033[m (${_user_selected} selected, $(wc -l < "$list_file") to write).."
|
|
fi
|
|
|
|
if [[ -s "$list_file" ]] ; then
|
|
# -d max_execution_time=0: re-validating and writing back every
|
|
# selected file of a large account can run well over an hour;
|
|
# without this the PHP CLI process is killed by PHP's own
|
|
# execution-time limit (typically inherited from the webserver's
|
|
# php.ini) mid-run.
|
|
if $DRY_RUN ; then
|
|
su -c "$PHP_BIN -d max_execution_time=0 $restore_php_file $_user $list_file --dry-run" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file"
|
|
else
|
|
su -c "$PHP_BIN -d max_execution_time=0 $restore_php_file $_user $list_file" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file"
|
|
fi
|
|
_rc=$?
|
|
else
|
|
echo -e "path\tbytes_written\tstatus\tdetail" > "$user_result_tsv"
|
|
_rc=0
|
|
fi
|
|
|
|
if [[ $_rc -ne 0 ]]; then
|
|
echo_failed
|
|
error "$(cat "$log_file")"
|
|
echo " [ FEHLER beim Restore-Lauf - siehe Server-Log ]" >> "$restore_report_file"
|
|
(( total_failed_users++ ))
|
|
continue
|
|
fi
|
|
|
|
echo_done
|
|
$terminal && echo ""
|
|
|
|
declare -i _user_ok=0
|
|
declare -i _user_ok_unverified=0
|
|
declare -i _user_write_errors=0
|
|
declare -a _user_write_error_lines=()
|
|
|
|
while IFS=$'\t' read -r _path _bytes _status _detail ; do
|
|
|
|
[[ "$_path" = "path" ]] && continue
|
|
[[ -z "$_path" ]] && continue
|
|
|
|
_origin="${_origin_of_path[$_path]:-VALID}"
|
|
|
|
case "$_status" in
|
|
OK|DRY_RUN)
|
|
(( _user_ok++ ))
|
|
[[ "$_origin" == "UNVERIFIED" ]] && (( _user_ok_unverified++ ))
|
|
;;
|
|
*) (( _user_write_errors++ )); _user_write_error_lines+=("${_path}"$'\t'"${_status}: ${_detail}") ;;
|
|
esac
|
|
|
|
# printf, not 'echo -e': $_detail can contain a raw PHP exception
|
|
# message (e.g. 'OCA\Encryption\Exceptions\...') - 'echo -e' would
|
|
# reinterpret those backslashes as escape sequences and silently
|
|
# mangle/eat parts of the text. printf's %s never reinterprets its
|
|
# argument, only the literal \t/\n in the format string itself.
|
|
printf '%s\t%s\t%s\t%s\t%s\n' "$_path" "$_bytes" "$_status" "$_origin" "$_detail" >> "$restore_report_file"
|
|
|
|
done < "$user_result_tsv"
|
|
|
|
_user_pct_ok="$(calc_percent "$_user_ok" "$_user_selected")"
|
|
|
|
if $terminal ; then
|
|
echo -e " \033[1;37mAccount ${_user}\033[m"
|
|
echo -e " Ausgewaehlt (VALID + plausibel UNVERIFIED).: ${_user_selected}"
|
|
if [[ $_user_selected_unverified -gt 0 ]] ; then
|
|
echo -e " davon 'Nicht pruefbar'/UNVERIFIED.......: \033[33m${_user_selected_unverified}\033[m"
|
|
fi
|
|
if [[ $_user_prevalidation_failed -gt 0 ]] ; then
|
|
echo -e " Vor dem Schreiben aussortiert..............: \033[33m${_user_prevalidation_failed}\033[m"
|
|
fi
|
|
if $DRY_RUN ; then
|
|
echo -e " Wuerde geschrieben & verifiziert werden....: \033[1;32m${_user_ok} (${_user_pct_ok} %)\033[m"
|
|
else
|
|
if [[ $_user_ok -gt 0 ]] ; then
|
|
echo -e " Zurueckgeschrieben & verifiziert...........: \033[1;32m${_user_ok} (${_user_pct_ok} %)\033[m"
|
|
else
|
|
echo -e " Zurueckgeschrieben & verifiziert...........: ${_user_ok} (${_user_pct_ok} %)"
|
|
fi
|
|
if [[ $_user_ok_unverified -gt 0 ]] ; then
|
|
echo -e " davon UNVERIFIED (bitte stichprobenartig pruefen): \033[33m${_user_ok_unverified}\033[m"
|
|
fi
|
|
fi
|
|
if [[ $_user_write_errors -gt 0 ]] ; then
|
|
echo -e " Fehler beim Schreiben/Verifizieren.........: \033[1;31m${_user_write_errors}\033[m"
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# - Problem files listed as their own, visually set-off block(s),
|
|
# - before the account statistics - same convention as
|
|
# - recover_bad_signature.sh.
|
|
# -
|
|
{
|
|
if [[ $_user_prevalidation_failed -gt 0 ]] ; then
|
|
echo ""
|
|
echo " ------------------------------------------------------------------"
|
|
echo " Vor dem Schreiben aussortiert - Account ${_user} (${_user_prevalidation_failed})"
|
|
echo " ------------------------------------------------------------------"
|
|
for _line in "${_user_prevalidation_failed_lines[@]}" ; do
|
|
printf ' %s\n' "$_line"
|
|
done
|
|
fi
|
|
if [[ $_user_write_errors -gt 0 ]] ; then
|
|
echo ""
|
|
echo " ------------------------------------------------------------------"
|
|
echo " Fehler beim Schreiben/Verifizieren - Account ${_user} (${_user_write_errors})"
|
|
echo " ------------------------------------------------------------------"
|
|
for _line in "${_user_write_error_lines[@]}" ; do
|
|
printf ' %s\n' "$_line"
|
|
done
|
|
fi
|
|
} >> "$restore_report_file"
|
|
|
|
{
|
|
echo ""
|
|
echo " Account ${_user}"
|
|
echo " Ausgewaehlt (VALID + plausibel UNVERIFIED).: ${_user_selected}"
|
|
[[ $_user_selected_unverified -gt 0 ]] && echo " davon 'Nicht pruefbar'/UNVERIFIED.......: ${_user_selected_unverified}"
|
|
[[ $_user_prevalidation_failed -gt 0 ]] && echo " Vor dem Schreiben aussortiert..............: ${_user_prevalidation_failed}"
|
|
if $DRY_RUN ; then
|
|
echo " Wuerde geschrieben & verifiziert werden....: ${_user_ok} (${_user_pct_ok} %)"
|
|
else
|
|
echo " Zurueckgeschrieben & verifiziert...........: ${_user_ok} (${_user_pct_ok} %)"
|
|
fi
|
|
[[ $_user_ok_unverified -gt 0 ]] && echo " davon UNVERIFIED (bitte pruefen).........: ${_user_ok_unverified}"
|
|
[[ $_user_write_errors -gt 0 ]] && echo " Fehler beim Schreiben/Verifizieren.........: ${_user_write_errors}"
|
|
} >> "$restore_report_file"
|
|
|
|
(( total_selected += _user_selected ))
|
|
(( total_selected_unverified += _user_selected_unverified ))
|
|
(( total_prevalidation_failed += _user_prevalidation_failed ))
|
|
(( total_ok += _user_ok ))
|
|
(( total_ok_unverified += _user_ok_unverified ))
|
|
(( total_write_errors += _user_write_errors ))
|
|
|
|
unset _user_valid _user_selected_unverified _user_prevalidation_failed _user_prevalidation_failed_lines
|
|
unset _user_ok _user_ok_unverified _user_write_errors _user_write_error_lines _origin_of_path
|
|
|
|
done
|
|
|
|
|
|
total_pct_ok="$(calc_percent "$total_ok" "$total_selected")"
|
|
|
|
{
|
|
echo ""
|
|
echo ""
|
|
echo "=================================================================="
|
|
echo " Gesamtergebnis"
|
|
echo "=================================================================="
|
|
echo ""
|
|
echo "Ausgewaehlte Accounts.......................: ${#selected_user_arr[@]}"
|
|
[[ $total_failed_users -gt 0 ]] && echo "Accounts mit Restore-Fehler.................: ${total_failed_users}"
|
|
echo "Ausgewaehlt (VALID + plausibel UNVERIFIED)..: ${total_selected}"
|
|
[[ $total_selected_unverified -gt 0 ]] && echo " davon 'Nicht pruefbar'/UNVERIFIED.........: ${total_selected_unverified}"
|
|
[[ $total_prevalidation_failed -gt 0 ]] && echo "Vor dem Schreiben aussortiert...............: ${total_prevalidation_failed}"
|
|
if $DRY_RUN ; then
|
|
echo "Wuerde geschrieben & verifiziert werden.....: ${total_ok} (${total_pct_ok} %)"
|
|
else
|
|
echo "Zurueckgeschrieben & verifiziert insgesamt...: ${total_ok} (${total_pct_ok} %)"
|
|
fi
|
|
[[ $total_ok_unverified -gt 0 ]] && echo " davon UNVERIFIED (bitte pruefen)..........: ${total_ok_unverified}"
|
|
[[ $total_write_errors -gt 0 ]] && echo "Fehler beim Schreiben/Verifizieren insgesamt: ${total_write_errors}"
|
|
echo ""
|
|
if $DRY_RUN ; then
|
|
echo "DRY-RUN: es wurde nichts geschrieben, gesichert oder veraendert."
|
|
else
|
|
echo "Chiffretext-Sicherung (vor dem Ueberschreiben) liegt unter: ${backup_dir}"
|
|
echo "Bitte Ergebnisse trotz Verifikation stichprobenartig pruefen - das gilt"
|
|
echo "besonders fuer die oben als UNVERIFIED markierten Dateien: dort gibt es"
|
|
echo "keine dedizierte Strukturpruefung, nur ein plausibles Groessenverhaeltnis."
|
|
fi
|
|
} >> "$restore_report_file"
|
|
|
|
blank_line
|
|
|
|
if $terminal ; then
|
|
echo -e "\033[37m\033[1mErgebnis\033[m"
|
|
echo ""
|
|
echo -e " Ausgewaehlte Accounts.......................: ${#selected_user_arr[@]}"
|
|
[[ $total_failed_users -gt 0 ]] && echo -e " Accounts mit Restore-Fehler.................: \033[1;31m${total_failed_users}\033[m"
|
|
echo -e " Ausgewaehlt (VALID + plausibel UNVERIFIED)..: ${total_selected}"
|
|
[[ $total_selected_unverified -gt 0 ]] && echo -e " davon 'Nicht pruefbar'/UNVERIFIED.........: \033[33m${total_selected_unverified}\033[m"
|
|
[[ $total_prevalidation_failed -gt 0 ]] && echo -e " Vor dem Schreiben aussortiert...............: \033[33m${total_prevalidation_failed}\033[m"
|
|
if $DRY_RUN ; then
|
|
echo -e " Wuerde geschrieben & verifiziert werden.....: \033[1;32m${total_ok} (${total_pct_ok} %)\033[m"
|
|
else
|
|
if [[ $total_ok -gt 0 ]] ; then
|
|
echo -e " Zurueckgeschrieben & verifiziert insgesamt...: \033[1;32m${total_ok} (${total_pct_ok} %)\033[m"
|
|
else
|
|
echo -e " Zurueckgeschrieben & verifiziert insgesamt...: ${total_ok} (${total_pct_ok} %)"
|
|
fi
|
|
[[ $total_ok_unverified -gt 0 ]] && echo -e " davon UNVERIFIED (bitte pruefen)..........: \033[33m${total_ok_unverified}\033[m"
|
|
fi
|
|
[[ $total_write_errors -gt 0 ]] && echo -e " Fehler beim Schreiben/Verifizieren insgesamt: \033[1;31m${total_write_errors}\033[m"
|
|
echo ""
|
|
echo -e " Restore-Report...............................: reports/$(basename "${restore_report_file}")"
|
|
if ! $DRY_RUN ; then
|
|
echo -e " Chiffretext-Sicherung........................: $backup_dir"
|
|
fi
|
|
echo ""
|
|
if $DRY_RUN ; then
|
|
info "Dry-run beendet - es wurde nichts geschrieben. Ohne '-n' (oder mit [2] bei der Modusabfrage) fuer den echten Restore erneut ausfuehren."
|
|
else
|
|
warn "Bitte die zurueckgeschriebenen Dateien trotz Verifikation stichprobenartig im Webinterface pruefen."
|
|
fi
|
|
fi
|
|
|
|
clean_up 0
|