After the user confirms with YES, each script now prints the paths of the two temporary files that are written continuously during the run, so they can be monitored with tail -f without having to know the paths by heart. Affects scan_, recover_, restore_, recreate_bad_signature.sh. recreate shows per-account log file names since it writes one per user. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GfXh5sRbEaXiEX6KjAPivc
1656 lines
58 KiB
Bash
Executable File
1656 lines
58 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"
|
||
|
||
# - Das Recovery-Verzeichnis wird bewusst NICHT unter script_dir
|
||
# - angelegt: liegen die Scripte z.B. unter /root/..., kann der
|
||
# - www-data-Prozess (der die entschluesselten Dateien tatsaechlich
|
||
# - schreibt) /root gar nicht erst durchqueren (Modus 700), selbst
|
||
# - wenn der Zielordner selbst an www-data gechownt wird - jedes
|
||
# - fopen() dort wuerde mit einem Fehler fehlschlagen. Stattdessen ein
|
||
# - eigener Pfad ausserhalb von /root und ausserhalb des Web-Docroots,
|
||
# - per conf-Datei ueberschreibbar (RECOVERY_BASE_DIR).
|
||
# -
|
||
DEFAULT_RECOVERY_BASE_DIR="/var/nc-recovery"
|
||
|
||
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)
|
||
|
||
_encryption_flag_changed=false
|
||
_orig_skip_check=""
|
||
|
||
|
||
# =============
|
||
# --- Some functions
|
||
# =============
|
||
|
||
usage() {
|
||
|
||
[[ -n "$1" ]] && error "$1"
|
||
|
||
[[ $terminal ]] && echo -e "
|
||
\033[1mUsage:\033[m
|
||
|
||
$(basename $0) -s <website>
|
||
|
||
\033[1mDescription\033[m
|
||
|
||
Attempts to recover files that were reported as 'Bad Signature' by
|
||
scan_bad_signature.sh, by temporarily ignoring the Server-Side
|
||
Encryption signature check (Nextcloud's own
|
||
'encryption_skip_signature_check' config switch) and decrypting the
|
||
affected files anyway.
|
||
|
||
This only works for files whose signature check fails due to a
|
||
computation mismatch, NOT for files whose encrypted content itself
|
||
is corrupted on disk - those will still produce garbage. Every
|
||
recovered file is therefore validated afterwards (file-type magic
|
||
bytes, PDF/zip integrity where possible) so you can tell the two
|
||
cases apart.
|
||
|
||
IMPORTANT:
|
||
- Original files are NEVER touched, modified or deleted. Every
|
||
recovered file is written to a SEPARATE directory outside of
|
||
Nextcloud's own storage (default: ${DEFAULT_RECOVERY_BASE_DIR}/<website>,
|
||
override with RECOVERY_BASE_DIR in the conf file) - the exact
|
||
path is shown before the confirmation prompt.
|
||
- The 'encryption_skip_signature_check' config switch is set
|
||
instance-wide for the duration of this script and restored to
|
||
its previous value afterwards (also on interrupt/error).
|
||
- Recovered files may contain unencrypted personal data outside
|
||
of Nextcloud's storage - handle and delete the recovery
|
||
directory accordingly once you are done reviewing it.
|
||
|
||
Unless '-V' is given on the command line, you will be asked
|
||
interactively whether to run a normal recovery or a revalidate-only
|
||
pass (see '-V' below). You will then be asked which existing scan
|
||
report (produced by scan_bad_signature.sh) to use, and then which
|
||
account(s) from that report to attempt recovery 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.
|
||
|
||
-V
|
||
Revalidate-only mode: does NOT touch 'encryption_skip_signature_check'
|
||
and does NOT decrypt anything again. Instead it re-runs the
|
||
(possibly improved) validation checks against files that were
|
||
already recovered by a previous, normal run - useful after this
|
||
script gained new file-type checks, without repeating the
|
||
instance-wide signature-skip window. 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
|
||
|
||
Re-validates already recovered files on 'cloud-01.oopen.de' with
|
||
the current checks, without decrypting anything again:
|
||
|
||
$(basename $0) -V -s cloud-01.oopen.de
|
||
"
|
||
|
||
clean_up 1
|
||
|
||
}
|
||
|
||
|
||
restore_encryption_flag() {
|
||
|
||
# - Setzt encryption_skip_signature_check auf den Ursprungswert
|
||
# - zurueck, egal ob das Skript normal durchlaeuft oder
|
||
# - unterbrochen wird. Kein-Op, falls das Flag nie geaendert wurde.
|
||
# -
|
||
if [[ "$_encryption_flag_changed" = true ]] ; then
|
||
echononl " Restoring encryption_skip_signature_check.."
|
||
if [[ -z "$_orig_skip_check" ]] ; then
|
||
su -c "$PHP_BIN $INSTALL_DIR/occ config:system:delete encryption_skip_signature_check" -s /bin/bash $HTTP_USER > /dev/null 2> "$log_file"
|
||
else
|
||
su -c "$PHP_BIN $INSTALL_DIR/occ config:system:set encryption_skip_signature_check --value=$_orig_skip_check --type=boolean" -s /bin/bash $HTTP_USER > /dev/null 2> "$log_file"
|
||
fi
|
||
|
||
if [[ $? -eq 0 ]] ; then
|
||
echo_ok
|
||
_encryption_flag_changed=false
|
||
else
|
||
echo_failed
|
||
error "Could not restore encryption_skip_signature_check automatically! Please check/fix manually on the server: occ config:system:get encryption_skip_signature_check (should be '${_orig_skip_check:-<not set>}'). $(cat "$log_file")"
|
||
fi
|
||
fi
|
||
|
||
}
|
||
|
||
|
||
clean_up() {
|
||
|
||
# Perform program exit housekeeping
|
||
restore_encryption_flag
|
||
[[ -n "$recovery_php_file" ]] && rm -f "$recovery_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 (signature-skip
|
||
## - decrypted) file. Dispatches primarily on the original file
|
||
## - extension; falls back to content-based detection (libmagic /
|
||
## - 'file') for unknown or missing extensions, or as a last resort
|
||
## - a text/binary heuristic + size comparison. 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
|
||
# Run unzip inside setsid so it has no controlling terminal.
|
||
# Without setsid, a password-protected ZIP causes unzip to try
|
||
# opening /dev/tty to prompt for the password. When running in
|
||
# the background of a tmux session this generates SIGTTIN, which
|
||
# STOPS the process (ps state T). A stopped process cannot receive
|
||
# SIGTERM, so timeout hangs indefinitely waiting for a child that
|
||
# will never exit. With setsid the /dev/tty open fails immediately
|
||
# with ENXIO and unzip exits with a non-zero code instead.
|
||
# -k 5: send SIGKILL 5 s after SIGTERM in case the process is
|
||
# still alive (e.g. stopped or in uninterruptible sleep).
|
||
local _unzip_exit
|
||
timeout -k 5 120 setsid unzip -tq "$_f" < /dev/null > /dev/null 2>&1
|
||
_unzip_exit=$?
|
||
if [[ $_unzip_exit -eq 0 ]] ; then
|
||
echo "VALID|zip integrity ok"
|
||
elif [[ $_unzip_exit -eq 124 || $_unzip_exit -eq 137 ]] ; then
|
||
# 124 = killed by SIGTERM after timeout, 137 = killed by SIGKILL (128+9)
|
||
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 -k 5 120 setsid unzip -t "$_f" < /dev/null 2>&1 | grep -v '^Archive:' | grep -v '^[[:space:]]*$' | head -1)")"
|
||
if echo "$_badentry" | grep -qi "password\|encrypt\|need PK compat" ; then
|
||
echo "UNVERIFIED|zip is password-protected (cannot verify without password${_badentry:+; unzip says: ${_badentry}})"
|
||
else
|
||
echo "INVALID|zip integrity check failed${_badentry:+ (${_badentry})}"
|
||
fi
|
||
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. A raw "does the file end in FFD9" byte check
|
||
## - produces false positives: many real-world JPEGs (EXIF/thumbnail
|
||
## - trailers, app-specific padding, camera-written footers) legally carry
|
||
## - a few extra bytes AFTER the actual End-Of-Image marker, so the file's
|
||
## - very last 2 bytes are often NOT ffd9 even though the image is 100%
|
||
## - intact and every viewer opens it fine. We therefore prefer an actual
|
||
## - JPEG decoder (which only cares whether the compressed image data is
|
||
## - intact, not what trails after it) and only fall back to the old
|
||
## - marker heuristic if no decoder tool is available.
|
||
## - 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): use 'file'/libmagic to see what the content
|
||
## - actually looks like. If it is confidently one of our known strong
|
||
## - types (this can happen for misnamed files, e.g. a '.pdf' saved
|
||
## - without the dot), re-run the matching dedicated check instead of
|
||
## - just guessing. Otherwise report the detected type plus a
|
||
## - text/binary plausibility check - never auto-classifies as INVALID
|
||
## - purely from an unrecognised type, since libmagic not knowing a
|
||
## - rare/proprietary format is not proof of corruption.
|
||
## -
|
||
_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
|
||
|
||
# - Kein starker Treffer ueber libmagic: zumindest grob zwischen
|
||
# - "sieht nach Text aus" und "enthaelt binaere/Steuerzeichen"
|
||
# - unterscheiden - reine Zufalls-/Entschluesselungs-Reste sind so
|
||
# - gut wie nie durchgehend druckbarer Text.
|
||
# -
|
||
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
|
||
|
||
# Note: recovered (plaintext) is ALWAYS smaller than original
|
||
# (ciphertext on disk, incl. IV/signature/padding overhead per 8192-byte
|
||
# block) - roughly 74-86% of it for this instance. That's normal and
|
||
# expected, not a sign of corruption; we only flag it if the ratio is
|
||
# implausible (e.g. way too small, suggesting truncation).
|
||
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 (chown/chmod of the recovery
|
||
# - directory, 'su' into the webserver user, 'occ config:system:set').
|
||
# - Fail fast with a clear message instead of a confusing permission
|
||
# - error somewhere in the middle of the run.
|
||
# -
|
||
if [[ "$(id -u)" -ne 0 ]] ; then
|
||
fatal "This script must be run as root (it needs to chown/chmod the recovery directory, 'su' into the webserver user and run 'occ config:system:set'). 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
|
||
# -------------
|
||
REVALIDATE_ONLY=false
|
||
_revalidate_only_explicit=false
|
||
|
||
while getopts hVs: opt ; do
|
||
case $opt in
|
||
h) usage ;;
|
||
V) REVALIDATE_ONLY=true; _revalidate_only_explicit=true ;;
|
||
s) WEBSITE=$OPTARG ;;
|
||
\?) usage
|
||
esac
|
||
done
|
||
|
||
|
||
# =============
|
||
# --- Ask which mode to run in, unless '-V' was already given on the
|
||
# --- command line (that still works unchanged for scripted/non-
|
||
# --- interactive invocations).
|
||
# =============
|
||
if ! $_revalidate_only_explicit && $terminal ; then
|
||
|
||
blank_line
|
||
echo -e "\033[37m\033[1mWhich mode should this run use?\033[m"
|
||
echo ""
|
||
echo -e " \033[1m[1] Recovery\033[m - decrypt bad-signature files (temporarily skips the signature check), write them under ${DEFAULT_RECOVERY_BASE_DIR}/<website> and validate them"
|
||
echo " [2] Revalidate-only - re-run just the validation checks against files a previous recovery run already wrote to disk (same as '-V'); nothing is decrypted again and no config value is touched"
|
||
info "Just press Return to use the default: [1] Recovery."
|
||
echo -n " Select mode by number [1]: "
|
||
read _mode_choice
|
||
|
||
case "$(trim "$_mode_choice")" in
|
||
2) REVALIDATE_ONLY=true ;;
|
||
""|1) REVALIDATE_ONLY=false ;;
|
||
*) 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)"
|
||
|
||
# - Recovery-Zielverzeichnis: bewusst ausserhalb von script_dir/WEB_BASE_DIR
|
||
# - (siehe Kommentar bei DEFAULT_RECOVERY_BASE_DIR weiter oben) - eine
|
||
# - per-Website Unterordner, den 'www-data' auch tatsaechlich erreichen
|
||
# - kann.
|
||
# -
|
||
[[ -n "$RECOVERY_BASE_DIR" ]] || RECOVERY_BASE_DIR=$DEFAULT_RECOVERY_BASE_DIR
|
||
recovery_dir="${RECOVERY_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
|
||
|
||
#clear
|
||
|
||
if $terminal ; then
|
||
echo ""
|
||
echo -e "\033[32m-----\033[m"
|
||
echo -e "Attempt to recover \033[1mBad Signature\033[m files on system \033[1m${WEB_BASE_DIR}\033[m"
|
||
echo -e "\033[1m
|
||
Original files are never touched. Recovered content is written to a
|
||
separate directory and must be reviewed manually.\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
|
||
# =============
|
||
|
||
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"
|
||
fi
|
||
|
||
if [[ "$ENCRYPTION_ENABLED" != "yes" && "$REVALIDATE_ONLY" = false ]] ; then
|
||
fatal "Server-Side Encryption is not enabled on ${WEBSITE} - a signature-skip recovery attempt makes no sense here."
|
||
fi
|
||
|
||
|
||
# =============
|
||
# --- Determine available bad-signature reports
|
||
# =============
|
||
|
||
mapfile -t _available_reports < <(ls -t "${report_dir}"/bad_signature_*.tsv 2> /dev/null)
|
||
|
||
if [[ ${#_available_reports[@]} -eq 0 ]] ; then
|
||
fatal "No scan report files (bad_signature_*.tsv) found in '${report_dir}'. Run scan_bad_signature.sh first."
|
||
fi
|
||
|
||
blank_line
|
||
if $terminal ; then
|
||
echo -e "\033[37m\033[1mAvailable bad-signature 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))]}"
|
||
|
||
|
||
# =============
|
||
# --- Determine accounts contained in the chosen report
|
||
# =============
|
||
|
||
unsorted_account_arr=($(awk -F'\t' 'NF==5 && $3 ~ /^[0-9]+$/ {print $1}' "$chosen_report" | sort -u))
|
||
IFS=$'\n' account_arr=($(sort <<<"${unsorted_account_arr[*]}"))
|
||
IFS=$CUR_IFS
|
||
|
||
if [[ ${#account_arr[@]} -eq 0 ]] ; then
|
||
fatal "No bad-signature entries found in '$(basename "$chosen_report")'."
|
||
fi
|
||
|
||
blank_line
|
||
if $terminal ; then
|
||
echo -e "\033[37m\033[1mAccounts with 'Bad Signature' entries in this report\033[m"
|
||
echo ""
|
||
for _a in "${account_arr[@]}" ; do
|
||
echo " - $_a"
|
||
done
|
||
echo ""
|
||
fi
|
||
|
||
echo -n " Account(s) to attempt recovery 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 attempt recovery for (blank separated, or 'all'): "
|
||
read _input
|
||
|
||
done
|
||
|
||
|
||
# =============
|
||
# --- Confirmation
|
||
# =============
|
||
|
||
mkdir -p "$report_dir" 2> /dev/null
|
||
recovery_report_file="${report_dir}/recovery_${WEBSITE}_${run_date}.tsv"
|
||
|
||
if $terminal ; then
|
||
echo ""
|
||
if $REVALIDATE_ONLY ; then
|
||
echo -e "\033[1;32mStarting revalidate-only run for \033[1;37m${WEBSITE}\033[m"
|
||
else
|
||
echo -e "\033[1;32mStarting recovery attempt for \033[1;37m${WEBSITE}\033[m"
|
||
fi
|
||
echo ""
|
||
echo -e " Cloud instance to be scanned.........: $WEBSITE"
|
||
echo ""
|
||
echo -e " Report used...........................: $(basename "$chosen_report")"
|
||
echo -e " Account(s) to recover.................: \033[33m${selected_user_arr[*]}\033[m"
|
||
echo ""
|
||
if $REVALIDATE_ONLY ; then
|
||
echo -e " Existing recovered files under........: $recovery_dir"
|
||
echo -e " Revalidate-only report..................: reports/$(basename "${recovery_report_file}")"
|
||
else
|
||
echo -e " Recovered files will be written to....: $recovery_dir"
|
||
echo -e " Recovery report........................: reports/$(basename "${recovery_report_file}")"
|
||
fi
|
||
echo ""
|
||
|
||
if $REVALIDATE_ONLY ; then
|
||
info "Revalidate-only mode: nothing is decrypted again, no config value is changed. Only the current (possibly newer) validation checks are re-run against files already recovered by a previous, normal run."
|
||
else
|
||
warn "This will TEMPORARILY set 'encryption_skip_signature_check' to true instance-wide, so already-broken files can be decrypted despite failing the signature check. It is restored to its previous value automatically when this script ends (also on Ctrl-C or error). Files with genuinely corrupted ciphertext will still come out as garbage - every recovered file is validated afterwards, but please still spot-check important results yourself before relying on them."
|
||
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 attempt recovery for each selected account on \033[1;37m$WEBSITE \033[m"
|
||
else
|
||
fatal "Abort by user request - Answer as not 'YES'"
|
||
fi
|
||
fi
|
||
|
||
if $terminal ; then
|
||
echo ""
|
||
echo -e " \033[1mTemporäre Dateien – werden laufend aktualisiert:\033[m"
|
||
echo ""
|
||
echo -e " Fortschritt / Debug-Meldungen (PHP-Ausgabe auf STDERR):"
|
||
echo -e " \033[1mtail -f ${log_file}\033[m"
|
||
echo ""
|
||
echo -e " Rohergebnis pro Account (PHP-Ausgabe auf STDOUT, Datei für Datei):"
|
||
echo -e " \033[1mtail -f ${LOCK_DIR}/result_<account>.tsv\033[m"
|
||
echo ""
|
||
fi
|
||
|
||
|
||
{
|
||
echo "=================================================================="
|
||
echo " Bad-Signature-Recovery-Versuch ${WEBSITE} ${run_date}"
|
||
echo "=================================================================="
|
||
echo ""
|
||
echo " Basis-Report: $(basename "$chosen_report")"
|
||
echo " Wiederhergestellte Dateien liegen unter: ${recovery_dir}"
|
||
echo ""
|
||
echo -e "path\trecovered_bytes\toriginal_size\tstatus\tvalidation\tdetail"
|
||
} > "$recovery_report_file"
|
||
|
||
|
||
if ! $REVALIDATE_ONLY ; then
|
||
|
||
# =============
|
||
# --- Temporarily enable encryption_skip_signature_check
|
||
# =============
|
||
|
||
blank_line
|
||
echononl " Reading current encryption_skip_signature_check value.."
|
||
_orig_skip_check="$(su -c "$PHP_BIN $INSTALL_DIR/occ config:system:get encryption_skip_signature_check" -s /bin/bash $HTTP_USER 2> /dev/null)"
|
||
_orig_skip_check="$(trim "$_orig_skip_check")"
|
||
echo_done
|
||
|
||
echononl " Enabling encryption_skip_signature_check (temporary)..."
|
||
su -c "$PHP_BIN $INSTALL_DIR/occ config:system:set encryption_skip_signature_check --value=true --type=boolean" -s /bin/bash $HTTP_USER > /dev/null 2> "$log_file"
|
||
if [[ $? -eq 0 ]] ; then
|
||
echo_ok
|
||
_encryption_flag_changed=true
|
||
trap 'restore_encryption_flag; clean_up 1' SIGHUP SIGINT SIGTERM
|
||
else
|
||
echo_failed
|
||
fatal "Could not enable encryption_skip_signature_check: $(cat "$log_file")"
|
||
fi
|
||
|
||
fi
|
||
|
||
|
||
if ! $REVALIDATE_ONLY ; then
|
||
|
||
# =============
|
||
# --- Write the embedded PHP recovery script
|
||
# =============
|
||
|
||
recovery_php_file="${INSTALL_DIR}/.recover_bad_signature_$$.php"
|
||
|
||
cat > "$recovery_php_file" <<'PHP_RECOVER_EOF'
|
||
<?php
|
||
/**
|
||
* recover_bad_signature.php
|
||
*
|
||
* Attempts to decrypt a known list of 'Bad Signature' files while
|
||
* 'encryption_skip_signature_check' is enabled (set by the calling
|
||
* wrapper script), and writes the resulting bytes to a separate
|
||
* output directory. Never touches the original file.
|
||
*
|
||
* Usage: php recover_bad_signature.php <uid> <listFile> <outDir>
|
||
* listFile: TSV with "path\tsize\tmtime" per line (no header)
|
||
*/
|
||
|
||
error_reporting(E_ALL);
|
||
ini_set('display_errors', '1');
|
||
|
||
fwrite(STDERR, "DEBUG: Recovery-Skript gestartet, PID=" . getmypid() . "\n");
|
||
|
||
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 recovering 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;
|
||
$outDir = $argv[3] ?? null;
|
||
|
||
if ($uid === null || $listFile === null || $outDir === null) {
|
||
fwrite(STDERR, "Usage: php recover_bad_signature.php <uid> <listFile> <outDir>\n");
|
||
exit(1);
|
||
}
|
||
if (!is_file($listFile)) {
|
||
fwrite(STDERR, "Liste nicht gefunden: $listFile\n");
|
||
exit(1);
|
||
}
|
||
if (!is_dir($outDir)) {
|
||
@mkdir($outDir, 0750, true);
|
||
}
|
||
|
||
$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\trecovered_bytes\toriginal_size\tstatus\n";
|
||
|
||
foreach ($lines as $line) {
|
||
$cols = explode("\t", $line);
|
||
$path = $cols[0] ?? '';
|
||
$origSize = $cols[1] ?? '';
|
||
if ($path === '') {
|
||
continue;
|
||
}
|
||
$total++;
|
||
|
||
try {
|
||
$node = $rootFolder->get($path);
|
||
} catch (\Throwable $e) {
|
||
$failed++;
|
||
$msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage());
|
||
echo "$path\t0\t$origSize\tNOT_FOUND: $msg\n";
|
||
flush();
|
||
continue;
|
||
}
|
||
|
||
if (!($node instanceof \OCP\Files\File)) {
|
||
$failed++;
|
||
echo "$path\t0\t$origSize\tNOT_A_FILE\n";
|
||
flush();
|
||
continue;
|
||
}
|
||
|
||
// Zielpfad unterhalb outDir, gleiche relative Struktur wie
|
||
// innerhalb der Nextcloud-Dateien des Users (ohne /<uid>/files/)
|
||
$relative = preg_replace('#^/' . preg_quote($uid, '#') . '/files/#', '', $path);
|
||
if ($relative === null || $relative === '') {
|
||
$relative = ltrim($path, '/');
|
||
}
|
||
$targetPath = rtrim($outDir, '/') . '/' . $relative;
|
||
$targetDir = dirname($targetPath);
|
||
if (!is_dir($targetDir)) {
|
||
@mkdir($targetDir, 0750, true);
|
||
}
|
||
|
||
try {
|
||
$in = $node->fopen('r');
|
||
if ($in === false) {
|
||
throw new \RuntimeException('fopen lieferte false');
|
||
}
|
||
$out = fopen($targetPath, 'w');
|
||
if ($out === false) {
|
||
throw new \RuntimeException("Zieldatei konnte nicht geoeffnet werden: $targetPath");
|
||
}
|
||
$bytes = stream_copy_to_stream($in, $out);
|
||
fclose($in);
|
||
fclose($out);
|
||
|
||
if ($bytes === false) {
|
||
throw new \RuntimeException('stream_copy_to_stream schlug fehl');
|
||
}
|
||
|
||
$ok++;
|
||
echo "$path\t$bytes\t$origSize\tOK\n";
|
||
flush();
|
||
} catch (\Throwable $e) {
|
||
$failed++;
|
||
$msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage());
|
||
echo "$path\t0\t$origSize\tERROR: " . get_class($e) . ": $msg\n";
|
||
flush();
|
||
@unlink($targetPath);
|
||
}
|
||
}
|
||
|
||
fwrite(STDERR, "\nFertig. Dateien verarbeitet: $total, erfolgreich gelesen: $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_RECOVER_EOF
|
||
|
||
chmod 644 "$recovery_php_file"
|
||
|
||
fi
|
||
|
||
|
||
# -----
|
||
# - 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 $REVALIDATE_ONLY ; then
|
||
if [[ ! -d "$recovery_dir" ]] ; then
|
||
fatal "Revalidate-only mode: recovery directory '$recovery_dir' does not exist - run this script without -V first."
|
||
fi
|
||
else
|
||
|
||
mkdir -p "$recovery_dir" 2> /dev/null
|
||
|
||
# - 'mkdir -p' creates missing parent directories (e.g. RECOVERY_BASE_DIR
|
||
# - itself) subject to the current umask - if that happens to be
|
||
# - restrictive, the PARENT of recovery_dir can end up blocking
|
||
# - traversal for $HTTP_USER even though recovery_dir itself is fine.
|
||
# - Explicitly (re-)open both levels for traversal (RECOVERY_BASE_DIR is
|
||
# - only ever traversed, never written into directly).
|
||
# -
|
||
chmod 755 "$RECOVERY_BASE_DIR" 2> /dev/null
|
||
chmod 755 "$recovery_dir" 2> /dev/null
|
||
|
||
# - recovery_dir itself still belongs to root at this point though -
|
||
# - mode 755 only grants $HTTP_USER (as 'other', not the owner) read +
|
||
# - traverse, NOT write. Files are later written per-account into
|
||
# - freshly created (and per-account chown'ed) subdirectories, but
|
||
# - recovery_dir itself also needs to be owned by $HTTP_USER so the
|
||
# - write test below (and 'mkdir' calls done by the PHP process
|
||
# - running as $HTTP_USER) can actually create things directly in it.
|
||
# -
|
||
chown "$HTTP_USER":"$HTTP_GROUP" "$recovery_dir" 2> /dev/null
|
||
|
||
# - Frueher, klarer Fehler statt 780x der gleiche stille Flop: pruefen,
|
||
# - ob der Webserver-User ($HTTP_USER) hier tatsaechlich schreiben kann,
|
||
# - BEVOR der eigentliche Recovery-Lauf beginnt. Faengt u.a. den Fall
|
||
# - ab, dass RECOVERY_BASE_DIR auf ein fuer $HTTP_USER unerreichbares
|
||
# - Verzeichnis (z.B. unterhalb von /root) gesetzt wurde.
|
||
# -
|
||
_write_test_file="${recovery_dir}/.write_test_$$"
|
||
su -c "touch '$_write_test_file'" -s /bin/bash $HTTP_USER 2> "$log_file"
|
||
if [[ ! -f "$_write_test_file" ]] ; then
|
||
fatal "Cannot write to recovery directory '$recovery_dir' as user '$HTTP_USER' - check the permissions of every path component (in particular that none of them, e.g. a parent directory, is only accessible to root). $(cat "$log_file")"
|
||
fi
|
||
rm -f "$_write_test_file" 2> /dev/null
|
||
|
||
fi
|
||
|
||
declare -i total_processed=0
|
||
declare -i total_valid=0
|
||
declare -i total_invalid=0
|
||
declare -i total_unverified=0
|
||
declare -i total_read_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"
|
||
} >> "$recovery_report_file"
|
||
|
||
list_file="${LOCK_DIR}/list_${_user}.tsv"
|
||
awk -F'\t' -v u="$_user" 'NF==5 && $1==u && $3 ~ /^[0-9]+$/ {print $2"\t"$3"\t"$4}' "$chosen_report" > "$list_file"
|
||
|
||
_user_total=$(wc -l < "$list_file")
|
||
|
||
if [[ $_user_total -eq 0 ]] ; then
|
||
warn "No bad-signature entries for account '${_user}' found in the selected report - skipping."
|
||
echo " [ keine Eintraege im gewaehlten Report ]" >> "$recovery_report_file"
|
||
continue
|
||
fi
|
||
|
||
user_out_dir="${recovery_dir}/${_user}"
|
||
user_result_tsv="${LOCK_DIR}/result_${_user}.tsv"
|
||
|
||
if $REVALIDATE_ONLY ; then
|
||
|
||
echononl " Revalidating account \033[1;37m${_user}\033[m (${_user_total} files).."
|
||
{
|
||
echo -e "path\trecovered_bytes\toriginal_size\tstatus"
|
||
while IFS=$'\t' read -r _p _osize _omtime ; do
|
||
[[ -z "$_p" ]] && continue
|
||
_rel="${_p#/${_user}/files/}"
|
||
_local="${user_out_dir}/${_rel}"
|
||
if [[ -f "$_local" ]] ; then
|
||
_b=$(stat -c%s "$_local" 2> /dev/null)
|
||
printf '%s\t%s\t%s\t%s\n' "$_p" "${_b:-0}" "$_osize" "OK"
|
||
else
|
||
printf '%s\t%s\t%s\t%s\n' "$_p" "0" "$_osize" "NOT_RECOVERED: no file found under ${user_out_dir} (run without -V first)"
|
||
fi
|
||
done < "$list_file"
|
||
} > "$user_result_tsv"
|
||
_rc=0
|
||
|
||
else
|
||
|
||
mkdir -p "$user_out_dir" 2> /dev/null
|
||
chown -R "$HTTP_USER":"$HTTP_GROUP" "$recovery_dir" 2> /dev/null
|
||
|
||
echononl " Recovering account \033[1;37m${_user}\033[m (${_user_total} files).."
|
||
# -d max_execution_time=0: decrypting/validating every 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.
|
||
su -c "$PHP_BIN -d max_execution_time=0 $recovery_php_file $_user $list_file $user_out_dir" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file"
|
||
_rc=$?
|
||
|
||
fi
|
||
|
||
if [[ $_rc -ne 0 ]]; then
|
||
echo_failed
|
||
error "$(cat "$log_file")"
|
||
echo " [ FEHLER beim Recovery-Lauf - siehe Server-Log ]" >> "$recovery_report_file"
|
||
(( total_failed_users++ ))
|
||
continue
|
||
fi
|
||
|
||
echo_done
|
||
$terminal && echo ""
|
||
|
||
declare -i _user_valid=0
|
||
declare -i _user_invalid=0
|
||
declare -i _user_unverified=0
|
||
declare -i _user_read_errors=0
|
||
declare -a _user_invalid_lines=()
|
||
declare -a _user_unverified_lines=()
|
||
|
||
while IFS=$'\t' read -r _path _recbytes _origsize _status ; do
|
||
|
||
[[ "$_path" = "path" ]] && continue
|
||
[[ -z "$_path" ]] && continue
|
||
|
||
if [[ "$_status" == OK* ]] ; then
|
||
_rel="${_path#/${_user}/files/}"
|
||
_local="${user_out_dir}/${_rel}"
|
||
_val_result="$(validate_recovered_file "$_local" "$_path" "$_origsize")"
|
||
_val_class="${_val_result%%|*}"
|
||
_val_detail="${_val_result#*|}"
|
||
case "$_val_class" in
|
||
VALID) (( _user_valid++ )) ;;
|
||
INVALID) (( _user_invalid++ )); _user_invalid_lines+=("${_path}"$'\t'"${_val_detail}") ;;
|
||
*) (( _user_unverified++ )); _user_unverified_lines+=("${_path}"$'\t'"${_val_detail}") ;;
|
||
esac
|
||
else
|
||
_val_class="READ_ERROR"
|
||
_val_detail="$_status"
|
||
(( _user_read_errors++ ))
|
||
fi
|
||
|
||
# printf, not 'echo -e': $_val_detail (or $_status for a
|
||
# READ_ERROR row) 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 in the format string itself.
|
||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "$_recbytes" "$_origsize" "$_status" "$_val_class" "$_val_detail" >> "$recovery_report_file"
|
||
|
||
done < "$user_result_tsv"
|
||
|
||
_user_pct_valid="$(calc_percent "$_user_valid" "$_user_total")"
|
||
|
||
if $terminal ; then
|
||
echo -e " \033[1;37mAccount ${_user}\033[m"
|
||
echo -e " Verarbeitete Dateien.....................: ${_user_total}"
|
||
if [[ $_user_valid -gt 0 ]] ; then
|
||
echo -e " Wiederhergestellt (valide)...............: \033[1;32m${_user_valid} (${_user_pct_valid} %)\033[m"
|
||
else
|
||
echo -e " Wiederhergestellt (valide)...............: ${_user_valid} (${_user_pct_valid} %)"
|
||
fi
|
||
if [[ $_user_invalid -gt 0 ]] ; then
|
||
echo -e " Datenmuell (ungueltig)...................: \033[1;31m${_user_invalid}\033[m"
|
||
fi
|
||
if [[ $_user_unverified -gt 0 ]] ; then
|
||
echo -e " Nicht pruefbar...........................: \033[33m${_user_unverified}\033[m"
|
||
fi
|
||
if [[ $_user_read_errors -gt 0 ]] ; then
|
||
echo -e " Weiterhin Lesefehler.....................: \033[1;31m${_user_read_errors}\033[m"
|
||
fi
|
||
echo ""
|
||
fi
|
||
|
||
# - 'Datenmuell' and 'Nicht pruefbar' files listed as their own,
|
||
# - visually set-off block(s), before the account statistics -
|
||
# - easier to scan/copy than digging them out of the full path list
|
||
# - above.
|
||
# -
|
||
{
|
||
if [[ $_user_invalid -gt 0 ]] ; then
|
||
echo ""
|
||
echo " ------------------------------------------------------------------"
|
||
echo " Datenmuell (ungueltig) - Account ${_user} (${_user_invalid})"
|
||
echo " ------------------------------------------------------------------"
|
||
for _line in "${_user_invalid_lines[@]}" ; do
|
||
printf ' %s\n' "$_line"
|
||
done
|
||
fi
|
||
if [[ $_user_unverified -gt 0 ]] ; then
|
||
echo ""
|
||
echo " ------------------------------------------------------------------"
|
||
echo " Nicht pruefbar - Account ${_user} (${_user_unverified})"
|
||
echo " ------------------------------------------------------------------"
|
||
for _line in "${_user_unverified_lines[@]}" ; do
|
||
printf ' %s\n' "$_line"
|
||
done
|
||
fi
|
||
} >> "$recovery_report_file"
|
||
|
||
{
|
||
echo ""
|
||
echo " Account ${_user}"
|
||
echo " Verarbeitete Dateien.....................: ${_user_total}"
|
||
echo " Wiederhergestellt (valide)...............: ${_user_valid} (${_user_pct_valid} %)"
|
||
[[ $_user_invalid -gt 0 ]] && echo " Datenmuell (ungueltig)...................: ${_user_invalid}"
|
||
[[ $_user_unverified -gt 0 ]] && echo " Nicht pruefbar...........................: ${_user_unverified}"
|
||
[[ $_user_read_errors -gt 0 ]] && echo " Weiterhin Lesefehler.....................: ${_user_read_errors}"
|
||
} >> "$recovery_report_file"
|
||
|
||
(( total_processed += _user_total ))
|
||
(( total_valid += _user_valid ))
|
||
(( total_invalid += _user_invalid ))
|
||
(( total_unverified += _user_unverified ))
|
||
(( total_read_errors += _user_read_errors ))
|
||
|
||
done
|
||
|
||
|
||
# =============
|
||
# --- Restore encryption_skip_signature_check as early as possible,
|
||
# --- before printing the overall summary.
|
||
# =============
|
||
|
||
restore_encryption_flag
|
||
|
||
|
||
total_pct_valid="$(calc_percent "$total_valid" "$total_processed")"
|
||
|
||
{
|
||
echo ""
|
||
echo ""
|
||
echo "=================================================================="
|
||
echo " Gesamtergebnis"
|
||
echo "=================================================================="
|
||
echo ""
|
||
echo "Gescannte Accounts..........................: ${#selected_user_arr[@]}"
|
||
[[ $total_failed_users -gt 0 ]] && echo "Accounts mit Recovery-Fehler................: ${total_failed_users}"
|
||
echo "Verarbeitete Dateien insgesamt..............: ${total_processed}"
|
||
echo "Wiederhergestellt insgesamt (valide)........: ${total_valid} (${total_pct_valid} %)"
|
||
[[ $total_invalid -gt 0 ]] && echo "Datenmuell insgesamt (ungueltig)............: ${total_invalid}"
|
||
[[ $total_unverified -gt 0 ]] && echo "Nicht pruefbar insgesamt....................: ${total_unverified}"
|
||
[[ $total_read_errors -gt 0 ]] && echo "Weiterhin Lesefehler insgesamt..............: ${total_read_errors}"
|
||
echo ""
|
||
echo "Wiederhergestellte Dateien liegen unter: ${recovery_dir}"
|
||
echo "Bitte Ergebnisse vor Weiterverwendung stichprobenartig pruefen,"
|
||
echo "und das Recovery-Verzeichnis danach sicher aufraeumen (enthaelt"
|
||
echo "unverschluesselte, ggf. sensible Nutzerdaten ausserhalb von Nextcloud)."
|
||
} >> "$recovery_report_file"
|
||
|
||
blank_line
|
||
|
||
if $terminal ; then
|
||
echo -e "\033[37m\033[1mErgebnis\033[m"
|
||
echo ""
|
||
echo -e " Gescannte Accounts..........................: ${#selected_user_arr[@]}"
|
||
[[ $total_failed_users -gt 0 ]] && echo -e " Accounts mit Recovery-Fehler................: \033[1;31m${total_failed_users}\033[m"
|
||
echo -e " Verarbeitete Dateien insgesamt..............: ${total_processed}"
|
||
if [[ $total_valid -gt 0 ]] ; then
|
||
echo -e " Wiederhergestellt insgesamt (valide)........: \033[1;32m${total_valid} (${total_pct_valid} %)\033[m"
|
||
else
|
||
echo -e " Wiederhergestellt insgesamt (valide)........: ${total_valid} (${total_pct_valid} %)"
|
||
fi
|
||
[[ $total_invalid -gt 0 ]] && echo -e " Datenmuell insgesamt (ungueltig)............: \033[1;31m${total_invalid}\033[m"
|
||
[[ $total_unverified -gt 0 ]] && echo -e " Nicht pruefbar insgesamt....................: \033[33m${total_unverified}\033[m"
|
||
[[ $total_read_errors -gt 0 ]] && echo -e " Weiterhin Lesefehler insgesamt..............: \033[1;31m${total_read_errors}\033[m"
|
||
echo ""
|
||
echo -e " Recovery-Verzeichnis.........................: $recovery_dir"
|
||
echo -e " Recovery-Report..............................: reports/$(basename "${recovery_report_file}")"
|
||
echo ""
|
||
warn "Wiederhergestellte Dateien vor Weiterverwendung stichprobenartig pruefen und das Recovery-Verzeichnis danach sicher aufraeumen - es enthaelt unverschluesselte, ggf. sensible Nutzerdaten ausserhalb von Nextcloud."
|
||
fi
|
||
|
||
clean_up 0
|