Compare commits

...
9 Commits
Author SHA1 Message Date
chris 69d038fe98 Make the mode selection clearer. 2026-09-17 11:10:53 +02:00
chris 5b811260a5 fix: update early signal trap in recover_bad_signature.sh to pass exit code
Line 1003: changed
       to
2026-09-17 00:58:49 +02:00
chrisandClaude Sonnet 4.6 dfacd0ce83 fix/feat: validation improvements and EXIT trap for all bad-signature scripts
fix: treat qpdf exit code 3 (warnings only) as VALID in PDF check
  - exit 0 and exit 3 both map to VALID; only exit 2 is a structural error
  - error detail now includes first matching error line from qpdf output

fix: add EXIT trap so encryption flag and temp files are always cleaned up
  - changed signal trap from  to
  - EXIT fires for any bash exit including syntax errors and unexpected crashes
  - clean_up() now runs  first to prevent re-entry / infinite loop
  - applies to recover_bad_signature.sh (where the flag matters most),
    restore_bad_signature.sh and recreate_bad_signature.sh

feat: check optional validation tools at startup and offer apt install
  - new check_optional_validation_tools() prompts [j/N] in interactive mode
  - covers: imagemagick (identify), ffmpeg (ffprobe), mp3val, flac, vorbis-tools (ogginfo)

feat: use optional tools for deeper file validation when available
  - PNG / GIF / BMP / TIFF: identify -regard-warnings (full decode) with magic-byte fallback
  - MP4 / MOV / M4V: ffprobe -show_streams (container parse) with ftyp-box fallback
  - MP3: new dedicated case — mp3val frame check with ID3/sync-word fallback
  - FLAC: new dedicated case — flac --silent --test with fLaC-signature fallback
  - OGG / OGA / OGV / OPUS: new dedicated case — ogginfo with OggS-signature fallback

Affects: recover_bad_signature.sh, restore_bad_signature.sh, recreate_bad_signature.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GfXh5sRbEaXiEX6KjAPivc
2026-09-16 19:06:03 +02:00
chris 8234c2d9f3 fix: add EXIT trap to ensure encryption flag and temp files are cleaned up on crash 2026-09-16 14:10:47 +02:00
chrisandClaude Sonnet 4.6 8bfc4d04b7 feat(ux): show temp log file paths after YES confirmation in all scripts
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
2026-09-16 09:20:55 +02:00
chris eef48a9c8d fix(validate): fix unzip hanging on password-protected ZIPs via setsid
unzip -tq on a password-protected archive tries to open /dev/tty to
prompt for the password. Inside a tmux session this generates SIGTTIN,
which stops the process (ps state T). A stopped process cannot receive
SIGTERM, so timeout waited indefinitely for a child that would never exit.

Fix: wrap all unzip calls with setsid so the process runs in a new
session without a controlling terminal. The /dev/tty open then fails
immediately with ENXIO and unzip exits with a non-zero code instead of
blocking. Additional hardening:
- timeout -k 5: send SIGKILL 5 s after SIGTERM as a last resort
- < /dev/null: also cut off stdin as a secondary safeguard
- exit 137 (128+9, SIGKILL) treated as timeout alongside 124
- error output matching "password"/"encrypt"/"need PK compat" reported
  as UNVERIFIED instead of INVALID

Affects: recover_bad_signature.sh, restore_bad_signature.sh,
         recreate_bad_signature.sh
2026-09-16 08:55:50 +02:00
chris 6677f4f0c2 fix(validate): add 120s timeout to unzip integrity checks
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.
2026-09-16 00:44:02 +02:00
chris b33b859cf2 Fix: Vorzeitiger Abbruch bei großen Accounts nach 3600s Laufzeit
Betroffen: scan_, recover_, restore_, recreate_bad_signature.sh
           und diagnose_share_key.sh

Bei Accounts mit sehr vielen bzw. sehr großen Dateien konnte der
jeweilige Pro-Account-Durchlauf länger als eine Stunde dauern und
wurde dann von PHP mit "Maximum execution time of 3600 seconds
exceeded" abgebrochen - mitten im Lauf, ohne jedes Ergebnis.

- su -c "$PHP_BIN ..." ruft PHP jetzt zusätzlich mit
  -d max_execution_time=0 auf.
- Das allein reicht nicht: Nextclouds eigenes lib/base.php setzt
  beim Bootstrap unbedingt (fest einprogrammiert, nicht
  konfigurierbar) set_time_limit(3600) und überschreibt damit den
  CLI-Flag wieder. Daher zusätzlich direkt nach dem require von
  lib/base.php ein erneutes set_time_limit(0) in jedem der fünf
  eingebetteten PHP-Scripte, das Nextclouds Reset seinerseits
  rückgängig macht.

Kein Verhaltensunterschied für kleine/normale Accounts, betrifft
nur die maximale Laufzeit pro Account.
2026-09-15 21:01:03 +02:00
chris 44f143fbe8 diagnose_share_key.sh: some changes on script output. 2026-09-15 01:17:59 +02:00
5 changed files with 927 additions and 105 deletions
+36 -9
View File
@@ -48,17 +48,30 @@ usage() {
fix - these are NOT signature problems, they are Server-Side
Encryption KEY-MATERIAL problems (a per-user 'share key' for a file
is either missing on disk or fails to decrypt with that user's
private key). This script accepts either a recovery_*.tsv or a
restore_*.tsv report as input.
private key).
\033[1mThis script accepts either of two report types as input, and\033[m
\033[1mevaluates a different row/status from each:\033[m
\033[1mrecovery_*.tsv\033[m report (from recover_bad_signature.sh)
-> evaluates every row whose validation is \033[1mREAD_ERROR\033[m
\033[1mrestore_*.tsv\033[m report (from restore_bad_signature.sh)
-> evaluates every row whose status is \033[1mWRITE_ERROR\033[m
This script changes NOTHING. It never decrypts a file, never
touches 'encryption_skip_signature_check', and never writes
anywhere except its own report file. It only:
1. Reads a chosen recovery_*.tsv report (produced by
recover_bad_signature.sh) and picks out every row whose
validation is READ_ERROR and whose detail text matches one of
the two known key-material error signatures above.
1. Reads the chosen report and picks out the matching rows:
- from a \033[1mrecovery_*.tsv\033[m report: every row whose
validation is \033[1mREAD_ERROR\033[m
- from a \033[1mrestore_*.tsv\033[m report: every row whose
status is \033[1mWRITE_ERROR\033[m
...and, in both cases, whose detail text matches one of the
two known key-material error signatures above.
2. For each such file, resolves - via Nextcloud's normal,
read-only Files API (\$node->getOwner(), \$folder->getById())
- who currently owns the file, i.e. whether the affected
@@ -405,8 +418,11 @@ if $terminal ; then
Nextcloud storage and never touches 'encryption_skip_signature_check'.
It only inspects, on disk and via the normal Files API, whether the
expected encryption key files are present for accounts/files that
recover_bad_signature.sh could not read (READ_ERROR, not 'Bad
Signature').\033[m"
recover_bad_signature.sh or restore_bad_signature.sh could not
process (not 'Bad Signature').\033[m"
echo ""
echo -e " \033[1mrecovery_*.tsv\033[m report -> evaluates \033[1mREAD_ERROR\033[m rows"
echo -e " \033[1mrestore_*.tsv\033[m report -> evaluates \033[1mWRITE_ERROR\033[m rows"
echo -e "\033[32m-----\033[m"
fi
@@ -629,6 +645,13 @@ if ($oldWorkingDir === false) {
}
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 diagnosing a large number of files 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) {
@@ -775,7 +798,11 @@ for _user in "${selected_user_arr[@]}" ; do
owner_result_tsv="${LOCK_DIR}/owner_${_user}.tsv"
echononl " Resolving ownership for account \033[1;37m${_user}\033[m (${_user_selected} files).."
su -c "$PHP_BIN $diag_php_file $_user $path_list_file" -s /bin/bash $HTTP_USER > "$owner_result_tsv" 2> "$log_file"
# -d max_execution_time=0: resolving ownership/key-material for a
# large number of files 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 $diag_php_file $_user $path_list_file" -s /bin/bash $HTTP_USER > "$owner_result_tsv" 2> "$log_file"
_rc=$?
if [[ $_rc -ne 0 ]]; then
+298 -33
View File
@@ -148,6 +148,8 @@ restore_encryption_flag() {
clean_up() {
# Perform program exit housekeeping
# Clear EXIT trap first so that the exit below does not fire it again.
trap - EXIT
restore_encryption_flag
[[ -n "$recovery_php_file" ]] && rm -f "$recovery_php_file" 2> /dev/null
rm -rf "$LOCK_DIR"
@@ -155,6 +157,82 @@ clean_up() {
exit $1
}
check_optional_validation_tools() {
# Checks whether optional tools that improve file-validation quality
# are installed. In interactive mode the user is offered to install
# any that are missing via apt. In non-interactive mode the function
# is a quiet no-op (missing tools degrade quality but do not abort).
local _missing_tools=()
local _missing_pkgs=()
local _desc=()
command -v identify > /dev/null 2>&1 || {
_missing_tools+=("identify")
_missing_pkgs+=("imagemagick")
_desc+=("identify (imagemagick) — PNG / TIFF / BMP / GIF: vollst. Dekodierung statt nur Magic-Bytes")
}
command -v ffprobe > /dev/null 2>&1 || {
_missing_tools+=("ffprobe")
_missing_pkgs+=("ffmpeg")
_desc+=("ffprobe (ffmpeg) — MP4 / MOV / M4V: Container-Parsing statt nur ftyp-Box-Suche")
}
command -v mp3val > /dev/null 2>&1 || {
_missing_tools+=("mp3val")
_missing_pkgs+=("mp3val")
_desc+=("mp3val (mp3val) — MP3: Frame-Struktur-Check statt nur ID3-Signatur")
}
command -v flac > /dev/null 2>&1 || {
_missing_tools+=("flac")
_missing_pkgs+=("flac")
_desc+=("flac (flac) — FLAC: Decode-Test statt nur fLaC-Signatur")
}
command -v ogginfo > /dev/null 2>&1 || {
_missing_tools+=("ogginfo")
_missing_pkgs+=("vorbis-tools")
_desc+=("ogginfo (vorbis-tools) — OGG / OGA / OPUS: Container-Check statt nur OggS-Signatur")
}
[[ ${#_missing_tools[@]} -eq 0 ]] && return 0
if ! $terminal ; then
return 0
fi
local _pkg_list="${_missing_pkgs[*]}"
echo ""
echo -e " \033[33mOptionale Validierungs-Tools fehlen – Checks laufen mit reduzierter Genauigkeit:\033[m"
echo ""
local _d
for _d in "${_desc[@]}" ; do
echo " ${_d}"
done
echo ""
echo -n " Jetzt installieren? apt install ${_pkg_list} [j/N]: "
read -r _yn
echo ""
if [[ "$_yn" =~ ^[jJyY]$ ]] ; then
echononl " Installiere Pakete: ${_pkg_list}.."
# shellcheck disable=SC2086
if apt-get install -y ${_missing_pkgs[*]} > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
echo ""
echo " Bitte manuell installieren:"
echo " apt install ${_pkg_list}"
fi
else
echo " Übersprungen — Validierung läuft mit reduzierter Genauigkeit."
fi
echo ""
}
is_number() {
return $(test ! -z "${1##*[!0-9]*}" > /dev/null 2>&1);
@@ -303,37 +381,70 @@ validate_recovered_file() {
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"
if command -v identify > /dev/null 2>&1 ; then
# Full decode: catches truncated data, corrupt IDAT chunks, bad CRCs
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded PNG ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode PNG"
fi
else
echo "INVALID|PNG signature missing"
local _head
_head="$(head -c8 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
if [[ "$_head" = "89504e470d0a1a0a" ]] ; then
echo "VALID|PNG signature ok (identify not installed — no deep check)"
else
echo "INVALID|PNG signature missing"
fi
fi
;;
gif)
local _head6
_head6="$(head -c6 "$_f" 2> /dev/null)"
if [[ "$_head6" = "GIF87a" || "$_head6" = "GIF89a" ]] ; then
echo "VALID|GIF signature ok"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded GIF ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode GIF"
fi
else
echo "INVALID|GIF signature missing"
local _head6
_head6="$(head -c6 "$_f" 2> /dev/null)"
if [[ "$_head6" = "GIF87a" || "$_head6" = "GIF89a" ]] ; then
echo "VALID|GIF signature ok (identify not installed — no deep check)"
else
echo "INVALID|GIF signature missing"
fi
fi
;;
bmp)
if [[ "$(head -c2 "$_f" 2> /dev/null)" = "BM" ]] ; then
echo "VALID|BMP signature ok"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded BMP ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode BMP"
fi
else
echo "INVALID|BMP signature missing"
if [[ "$(head -c2 "$_f" 2> /dev/null)" = "BM" ]] ; then
echo "VALID|BMP signature ok (identify not installed — no deep check)"
else
echo "INVALID|BMP signature missing"
fi
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"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded TIFF ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode TIFF"
fi
else
echo "INVALID|TIFF signature missing"
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 (identify not installed — no deep check)"
else
echo "INVALID|TIFF signature missing"
fi
fi
;;
pnm|pgm|ppm|pbm)
@@ -360,22 +471,58 @@ validate_recovered_file() {
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
if unzip -tq "$_f" > /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 "$(unzip -t "$_f" 2>&1 | grep -v '^Archive:' | grep -v '^[[:space:]]*$' | head -1)")"
echo "INVALID|zip integrity check failed${_badentry:+ (${_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"
if command -v ffprobe > /dev/null 2>&1 ; then
# Full container parse: detects truncated/corrupt streams
local _ffprobe_out _ffprobe_exit
_ffprobe_out="$(ffprobe -v error -show_streams "$_f" 2>&1)"
_ffprobe_exit=$?
if [[ $_ffprobe_exit -eq 0 ]] ; then
local _streams
_streams="$(echo "$_ffprobe_out" | grep -c '\[STREAM\]' || true)"
echo "VALID|ffprobe parsed container ok (${_streams} stream(s) found)"
else
local _ffprobe_err
_ffprobe_err="$(echo "$_ffprobe_out" | head -1)"
echo "INVALID|ffprobe failed to parse container${_ffprobe_err:+ (${_ffprobe_err})}"
fi
else
echo "INVALID|mp4 ftyp box not found"
if head -c 64 "$_f" 2> /dev/null | grep -aq "ftyp" ; then
echo "VALID|mp4 ftyp box found (ffprobe not installed — no deep check)"
else
echo "INVALID|mp4 ftyp box not found"
fi
fi
;;
doc|xls|ppt|ole|msi)
@@ -475,6 +622,75 @@ validate_recovered_file() {
echo "INVALID|GIMP (gimp xcf) signature missing"
fi
;;
mp3)
if command -v mp3val > /dev/null 2>&1 ; then
# mp3val always exits 0 but prints "No errors found" on success
local _mp3val_out
_mp3val_out="$(mp3val "$_f" 2>&1)"
if echo "$_mp3val_out" | grep -q 'No errors found' ; then
echo "VALID|mp3val: no errors found"
else
local _mp3_err
_mp3_err="$(echo "$_mp3val_out" | grep -iv '^mp3val\|^$' | head -2 | tr '\n' ' ' | sed 's/ $//')"
echo "INVALID|mp3val reported errors${_mp3_err:+ (${_mp3_err})}"
fi
else
local _head3 _head2hex
_head3="$(head -c3 "$_f" 2>/dev/null)"
_head2hex="$(head -c2 "$_f" 2>/dev/null | od -An -tx1 | tr -d ' \n')"
if [[ "$_head3" = "ID3" ]] || echo "$_head2hex" | grep -qE '^fff[bef2]' ; then
echo "VALID|MP3 ID3 tag / MPEG sync ok (mp3val not installed — no deep check)"
else
echo "INVALID|MP3: no ID3 header or MPEG frame sync found"
fi
fi
;;
flac)
if command -v flac > /dev/null 2>&1 ; then
# --test decodes without writing output; exit 0 = file is intact
local _flac_out _flac_exit
_flac_out="$(flac --silent --test "$_f" 2>&1)"
_flac_exit=$?
if [[ $_flac_exit -eq 0 ]] ; then
echo "VALID|flac --test: ok"
else
local _flac_err
_flac_err="$(echo "$_flac_out" | grep -v '^$' | tail -1)"
echo "INVALID|flac --test failed${_flac_err:+ (${_flac_err})}"
fi
else
local _head4hex
_head4hex="$(head -c4 "$_f" 2>/dev/null | od -An -tx1 | tr -d ' \n')"
if [[ "$_head4hex" = "664c6143" ]] ; then # "fLaC"
echo "VALID|FLAC 'fLaC' signature ok (flac not installed — no deep check)"
else
echo "INVALID|FLAC 'fLaC' signature missing"
fi
fi
;;
ogg|oga|ogv|opus)
if command -v ogginfo > /dev/null 2>&1 ; then
# ogginfo exits 0 if the Ogg container is intact
local _ogg_out _ogg_exit
_ogg_out="$(ogginfo "$_f" 2>&1)"
_ogg_exit=$?
if [[ $_ogg_exit -eq 0 ]] ; then
echo "VALID|ogginfo: container parsed ok"
else
local _ogg_err
_ogg_err="$(echo "$_ogg_out" | grep -iv '^Processing\|^$' | head -2 | tr '\n' ' ' | sed 's/ $//')"
echo "INVALID|ogginfo failed${_ogg_err:+ (${_ogg_err})}"
fi
else
local _head4ogg
_head4ogg="$(head -c4 "$_f" 2>/dev/null)"
if [[ "$_head4ogg" = "OggS" ]] ; then
echo "VALID|OGG 'OggS' signature ok (ogginfo not installed — no deep check)"
else
echo "INVALID|OGG 'OggS' signature missing"
fi
fi
;;
wav)
local _riff _wave
_riff="$(head -c4 "$_f" 2> /dev/null)"
@@ -562,6 +778,15 @@ validate_recovered_file() {
## - PDF structural check shared by the 'pdf' case and the 'ai'
## - (PDF-compatible) case. Sets $_pdf_check_detail, returns 0/1.
## -
## - qpdf exit codes:
## - 0 no problems
## - 2 structural errors → file is genuinely corrupt/unreadable
## - 3 warnings only → minor spec non-conformances; every real
## - viewer opens the file without issues
## - Only exit code 2 is treated as INVALID here. Exit code 3 (warnings)
## - is reported as VALID with a note, preventing false positives for
## - real-world PDFs that have trivial non-conformances.
## -
_pdf_check() {
local _f="$1"
if [[ "$(head -c5 "$_f" 2> /dev/null)" != "%PDF-" ]] ; then
@@ -569,11 +794,22 @@ _pdf_check() {
return 1
fi
if command -v qpdf > /dev/null 2>&1 ; then
if qpdf --check "$_f" > /dev/null 2>&1 ; then
local _qpdf_out _qpdf_exit
_qpdf_out="$(qpdf --check "$_f" 2>&1)"
_qpdf_exit=$?
if [[ $_qpdf_exit -eq 0 ]] ; then
_pdf_check_detail="qpdf --check ok"
return 0
elif [[ $_qpdf_exit -eq 3 ]] ; then
# Warnings only — no structural errors. File is readable by all
# standard PDF viewers; non-conformances are minor/cosmetic.
_pdf_check_detail="qpdf --check ok (warnings only — file is readable)"
return 0
else
_pdf_check_detail="qpdf --check failed"
# Exit code 2 (or unexpected): genuine structural errors.
local _first_err
_first_err="$(echo "$_qpdf_out" | grep -i 'error' | head -1 | sed 's/^[[:space:]]*//')"
_pdf_check_detail="qpdf --check failed (exit ${_qpdf_exit}${_first_err:+: ${_first_err}})"
return 1
fi
fi
@@ -764,7 +1000,7 @@ 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
trap 'clean_up 1' SIGHUP SIGINT SIGTERM
else
@@ -814,12 +1050,13 @@ if ! $_revalidate_only_explicit && $terminal ; then
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 \033[1m${DEFAULT_RECOVERY_BASE_DIR}/<website>\033[m and validate them"
write them under
\033[1m${DEFAULT_RECOVERY_BASE_DIR}/<website>\033[m
and validate them"
echo ""
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"
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
@@ -1082,6 +1319,8 @@ done
mkdir -p "$report_dir" 2> /dev/null
recovery_report_file="${report_dir}/recovery_${WEBSITE}_${run_date}.tsv"
check_optional_validation_tools
if $terminal ; then
echo ""
if $REVALIDATE_ONLY ; then
@@ -1122,6 +1361,18 @@ if $terminal ; then
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 "=================================================================="
@@ -1152,7 +1403,10 @@ su -c "$PHP_BIN $INSTALL_DIR/occ config:system:set encryption_skip_signature_che
if [[ $? -eq 0 ]] ; then
echo_ok
_encryption_flag_changed=true
trap 'restore_encryption_flag; clean_up 1' SIGHUP SIGINT SIGTERM
# Trap signals AND normal/abnormal exit so that the encryption flag
# and temp files are always cleaned up — even on a syntax error or
# an unexpected crash (EXIT fires for any bash exit, including errors).
trap 'clean_up 1' SIGHUP SIGINT SIGTERM EXIT
else
echo_failed
fatal "Could not enable encryption_skip_signature_check: $(cat "$log_file")"
@@ -1204,6 +1458,13 @@ if ($oldWorkingDir === false) {
}
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) {
@@ -1438,7 +1699,11 @@ for _user in "${selected_user_arr[@]}" ; do
chown -R "$HTTP_USER":"$HTTP_GROUP" "$recovery_dir" 2> /dev/null
echononl " Recovering account \033[1;37m${_user}\033[m (${_user_total} files).."
su -c "$PHP_BIN $recovery_php_file $_user $list_file $user_out_dir" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file"
# -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
+283 -31
View File
@@ -183,6 +183,8 @@ usage() {
clean_up() {
# Perform program exit housekeeping
# Clear EXIT trap first so that the exit below does not fire it again.
trap - EXIT
[[ -n "$recreate_php_file" ]] && rm -f "$recreate_php_file" 2> /dev/null
rm -rf "$LOCK_DIR"
blank_line
@@ -190,6 +192,77 @@ clean_up() {
}
check_optional_validation_tools() {
local _missing_tools=()
local _missing_pkgs=()
local _desc=()
command -v identify > /dev/null 2>&1 || {
_missing_tools+=("identify")
_missing_pkgs+=("imagemagick")
_desc+=("identify (imagemagick) — PNG / TIFF / BMP / GIF: vollst. Dekodierung statt nur Magic-Bytes")
}
command -v ffprobe > /dev/null 2>&1 || {
_missing_tools+=("ffprobe")
_missing_pkgs+=("ffmpeg")
_desc+=("ffprobe (ffmpeg) — MP4 / MOV / M4V: Container-Parsing statt nur ftyp-Box-Suche")
}
command -v mp3val > /dev/null 2>&1 || {
_missing_tools+=("mp3val")
_missing_pkgs+=("mp3val")
_desc+=("mp3val (mp3val) — MP3: Frame-Struktur-Check statt nur ID3-Signatur")
}
command -v flac > /dev/null 2>&1 || {
_missing_tools+=("flac")
_missing_pkgs+=("flac")
_desc+=("flac (flac) — FLAC: Decode-Test statt nur fLaC-Signatur")
}
command -v ogginfo > /dev/null 2>&1 || {
_missing_tools+=("ogginfo")
_missing_pkgs+=("vorbis-tools")
_desc+=("ogginfo (vorbis-tools) — OGG / OGA / OPUS: Container-Check statt nur OggS-Signatur")
}
[[ ${#_missing_tools[@]} -eq 0 ]] && return 0
if ! $terminal ; then
return 0
fi
local _pkg_list="${_missing_pkgs[*]}"
echo ""
echo -e " \033[33mOptionale Validierungs-Tools fehlen – Checks laufen mit reduzierter Genauigkeit:\033[m"
echo ""
local _d
for _d in "${_desc[@]}" ; do
echo " ${_d}"
done
echo ""
echo -n " Jetzt installieren? apt install ${_pkg_list} [j/N]: "
read -r _yn
echo ""
if [[ "$_yn" =~ ^[jJyY]$ ]] ; then
echononl " Installiere Pakete: ${_pkg_list}.."
# shellcheck disable=SC2086
if apt-get install -y ${_missing_pkgs[*]} > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
echo ""
echo " Bitte manuell installieren:"
echo " apt install ${_pkg_list}"
fi
else
echo " Übersprungen — Validierung läuft mit reduzierter Genauigkeit."
fi
echo ""
}
is_number() {
return $(test ! -z "${1##*[!0-9]*}" > /dev/null 2>&1);
@@ -338,37 +411,69 @@ validate_recovered_file() {
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"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded PNG ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode PNG"
fi
else
echo "INVALID|PNG signature missing"
local _head
_head="$(head -c8 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
if [[ "$_head" = "89504e470d0a1a0a" ]] ; then
echo "VALID|PNG signature ok (identify not installed — no deep check)"
else
echo "INVALID|PNG signature missing"
fi
fi
;;
gif)
local _head6
_head6="$(head -c6 "$_f" 2> /dev/null)"
if [[ "$_head6" = "GIF87a" || "$_head6" = "GIF89a" ]] ; then
echo "VALID|GIF signature ok"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded GIF ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode GIF"
fi
else
echo "INVALID|GIF signature missing"
local _head6
_head6="$(head -c6 "$_f" 2> /dev/null)"
if [[ "$_head6" = "GIF87a" || "$_head6" = "GIF89a" ]] ; then
echo "VALID|GIF signature ok (identify not installed — no deep check)"
else
echo "INVALID|GIF signature missing"
fi
fi
;;
bmp)
if [[ "$(head -c2 "$_f" 2> /dev/null)" = "BM" ]] ; then
echo "VALID|BMP signature ok"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded BMP ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode BMP"
fi
else
echo "INVALID|BMP signature missing"
if [[ "$(head -c2 "$_f" 2> /dev/null)" = "BM" ]] ; then
echo "VALID|BMP signature ok (identify not installed — no deep check)"
else
echo "INVALID|BMP signature missing"
fi
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"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded TIFF ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode TIFF"
fi
else
echo "INVALID|TIFF signature missing"
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 (identify not installed — no deep check)"
else
echo "INVALID|TIFF signature missing"
fi
fi
;;
pnm|pgm|ppm|pbm)
@@ -386,22 +491,57 @@ validate_recovered_file() {
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
if unzip -tq "$_f" > /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 "$(unzip -t "$_f" 2>&1 | grep -v '^Archive:' | grep -v '^[[:space:]]*$' | head -1)")"
echo "INVALID|zip integrity check failed${_badentry:+ (${_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"
if command -v ffprobe > /dev/null 2>&1 ; then
local _ffprobe_out _ffprobe_exit
_ffprobe_out="$(ffprobe -v error -show_streams "$_f" 2>&1)"
_ffprobe_exit=$?
if [[ $_ffprobe_exit -eq 0 ]] ; then
local _streams
_streams="$(echo "$_ffprobe_out" | grep -c '\[STREAM\]' || true)"
echo "VALID|ffprobe parsed container ok (${_streams} stream(s) found)"
else
local _ffprobe_err
_ffprobe_err="$(echo "$_ffprobe_out" | head -1)"
echo "INVALID|ffprobe failed to parse container${_ffprobe_err:+ (${_ffprobe_err})}"
fi
else
echo "INVALID|mp4 ftyp box not found"
if head -c 64 "$_f" 2> /dev/null | grep -aq "ftyp" ; then
echo "VALID|mp4 ftyp box found (ffprobe not installed — no deep check)"
else
echo "INVALID|mp4 ftyp box not found"
fi
fi
;;
doc|xls|ppt|ole|msi)
@@ -493,6 +633,72 @@ validate_recovered_file() {
echo "INVALID|GIMP (gimp xcf) signature missing"
fi
;;
mp3)
if command -v mp3val > /dev/null 2>&1 ; then
local _mp3val_out
_mp3val_out="$(mp3val "$_f" 2>&1)"
if echo "$_mp3val_out" | grep -q 'No errors found' ; then
echo "VALID|mp3val: no errors found"
else
local _mp3_err
_mp3_err="$(echo "$_mp3val_out" | grep -iv '^mp3val\|^$' | head -2 | tr '\n' ' ' | sed 's/ $//')"
echo "INVALID|mp3val reported errors${_mp3_err:+ (${_mp3_err})}"
fi
else
local _head3 _head2hex
_head3="$(head -c3 "$_f" 2>/dev/null)"
_head2hex="$(head -c2 "$_f" 2>/dev/null | od -An -tx1 | tr -d ' \n')"
if [[ "$_head3" = "ID3" ]] || echo "$_head2hex" | grep -qE '^fff[bef2]' ; then
echo "VALID|MP3 ID3 tag / MPEG sync ok (mp3val not installed — no deep check)"
else
echo "INVALID|MP3: no ID3 header or MPEG frame sync found"
fi
fi
;;
flac)
if command -v flac > /dev/null 2>&1 ; then
local _flac_out _flac_exit
_flac_out="$(flac --silent --test "$_f" 2>&1)"
_flac_exit=$?
if [[ $_flac_exit -eq 0 ]] ; then
echo "VALID|flac --test: ok"
else
local _flac_err
_flac_err="$(echo "$_flac_out" | grep -v '^$' | tail -1)"
echo "INVALID|flac --test failed${_flac_err:+ (${_flac_err})}"
fi
else
local _head4hex
_head4hex="$(head -c4 "$_f" 2>/dev/null | od -An -tx1 | tr -d ' \n')"
if [[ "$_head4hex" = "664c6143" ]] ; then
echo "VALID|FLAC 'fLaC' signature ok (flac not installed — no deep check)"
else
echo "INVALID|FLAC 'fLaC' signature missing"
fi
fi
;;
ogg|oga|ogv|opus)
if command -v ogginfo > /dev/null 2>&1 ; then
local _ogg_out _ogg_exit
_ogg_out="$(ogginfo "$_f" 2>&1)"
_ogg_exit=$?
if [[ $_ogg_exit -eq 0 ]] ; then
echo "VALID|ogginfo: container parsed ok"
else
local _ogg_err
_ogg_err="$(echo "$_ogg_out" | grep -iv '^Processing\|^$' | head -2 | tr '\n' ' ' | sed 's/ $//')"
echo "INVALID|ogginfo failed${_ogg_err:+ (${_ogg_err})}"
fi
else
local _head4ogg
_head4ogg="$(head -c4 "$_f" 2>/dev/null)"
if [[ "$_head4ogg" = "OggS" ]] ; then
echo "VALID|OGG 'OggS' signature ok (ogginfo not installed — no deep check)"
else
echo "INVALID|OGG 'OggS' signature missing"
fi
fi
;;
wav)
local _riff _wave
_riff="$(head -c4 "$_f" 2> /dev/null)"
@@ -564,6 +770,15 @@ validate_recovered_file() {
esac
}
## - qpdf exit codes:
## - 0 no problems
## - 2 structural errors → file is genuinely corrupt/unreadable
## - 3 warnings only → minor spec non-conformances; every real
## - viewer opens the file without issues
## - Only exit code 2 is treated as INVALID here. Exit code 3 (warnings)
## - is reported as VALID with a note, preventing false positives for
## - real-world PDFs that have trivial non-conformances.
## -
_pdf_check() {
local _f="$1"
if [[ "$(head -c5 "$_f" 2> /dev/null)" != "%PDF-" ]] ; then
@@ -571,11 +786,22 @@ _pdf_check() {
return 1
fi
if command -v qpdf > /dev/null 2>&1 ; then
if qpdf --check "$_f" > /dev/null 2>&1 ; then
local _qpdf_out _qpdf_exit
_qpdf_out="$(qpdf --check "$_f" 2>&1)"
_qpdf_exit=$?
if [[ $_qpdf_exit -eq 0 ]] ; then
_pdf_check_detail="qpdf --check ok"
return 0
elif [[ $_qpdf_exit -eq 3 ]] ; then
# Warnings only — no structural errors. File is readable by all
# standard PDF viewers; non-conformances are minor/cosmetic.
_pdf_check_detail="qpdf --check ok (warnings only — file is readable)"
return 0
else
_pdf_check_detail="qpdf --check failed"
# Exit code 2 (or unexpected): genuine structural errors.
local _first_err
_first_err="$(echo "$_qpdf_out" | grep -i 'error' | head -1 | sed 's/^[[:space:]]*//')"
_pdf_check_detail="qpdf --check failed (exit ${_qpdf_exit}${_first_err:+: ${_first_err}})"
return 1
fi
fi
@@ -731,7 +957,9 @@ fi
# -
if mkdir "$LOCK_DIR" 2> /dev/null ; then
trap clean_up SIGHUP SIGINT SIGTERM
# Trap signals AND normal/abnormal exit so that temp files and the
# lock directory are always removed — even on an unexpected crash.
trap 'clean_up 1' SIGHUP SIGINT SIGTERM EXIT
else
@@ -785,9 +1013,8 @@ if ! $_dry_run_explicit && $terminal ; then
echo -e " \033[1m[1] Dry-run\033[m - go through everything (selection, re-validation, share check,
reporting), but delete, back up, create or verify NOTHING"
echo ""
echo " [2] Real recreate - actually back up, DELETE the broken file, create it fresh
and verify each one"
echo " [2] Real recreate - actually back up, DELETE the broken file, create it fresh
and verify each one"
info "Just press Return to use the default: [1] Dry-run."
echo -n " Select mode by number [1]: "
read _mode_choice
@@ -1041,6 +1268,8 @@ done
mkdir -p "$report_dir" 2> /dev/null
recreate_report_file="${report_dir}/recreate_${WEBSITE}_${run_date}.tsv"
check_optional_validation_tools
if $terminal ; then
echo ""
if $DRY_RUN ; then
@@ -1084,6 +1313,18 @@ if $terminal ; then
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, pro Account):"
echo -e " \033[1mtail -f ${LOCK_DIR}/${script_name%%.*}_<account>.log\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 "=================================================================="
@@ -1166,6 +1407,13 @@ if ($oldWorkingDir === false) {
}
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 recreating 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) {
@@ -1767,7 +2015,11 @@ for _user in "${selected_user_arr[@]}" ; do
_php_args=("$recreate_php_file" "$_user" "$list_file" "$DATA_DIR")
$DRY_RUN && _php_args+=("--dry-run")
$FORCE_SHARED && _php_args+=("--force")
su -c "$PHP_BIN ${_php_args[*]}" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file"
# -d max_execution_time=0: recreating 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.
su -c "$PHP_BIN -d max_execution_time=0 ${_php_args[*]}" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file"
_rc=$?
else
echo -e "path\tbytes_written\tstatus\tdetail" > "$user_result_tsv"
+283 -30
View File
@@ -167,6 +167,8 @@ usage() {
clean_up() {
# Perform program exit housekeeping
# Clear EXIT trap first so that the exit below does not fire it again.
trap - EXIT
[[ -n "$restore_php_file" ]] && rm -f "$restore_php_file" 2> /dev/null
rm -rf "$LOCK_DIR"
blank_line
@@ -174,6 +176,77 @@ clean_up() {
}
check_optional_validation_tools() {
local _missing_tools=()
local _missing_pkgs=()
local _desc=()
command -v identify > /dev/null 2>&1 || {
_missing_tools+=("identify")
_missing_pkgs+=("imagemagick")
_desc+=("identify (imagemagick) — PNG / TIFF / BMP / GIF: vollst. Dekodierung statt nur Magic-Bytes")
}
command -v ffprobe > /dev/null 2>&1 || {
_missing_tools+=("ffprobe")
_missing_pkgs+=("ffmpeg")
_desc+=("ffprobe (ffmpeg) — MP4 / MOV / M4V: Container-Parsing statt nur ftyp-Box-Suche")
}
command -v mp3val > /dev/null 2>&1 || {
_missing_tools+=("mp3val")
_missing_pkgs+=("mp3val")
_desc+=("mp3val (mp3val) — MP3: Frame-Struktur-Check statt nur ID3-Signatur")
}
command -v flac > /dev/null 2>&1 || {
_missing_tools+=("flac")
_missing_pkgs+=("flac")
_desc+=("flac (flac) — FLAC: Decode-Test statt nur fLaC-Signatur")
}
command -v ogginfo > /dev/null 2>&1 || {
_missing_tools+=("ogginfo")
_missing_pkgs+=("vorbis-tools")
_desc+=("ogginfo (vorbis-tools) — OGG / OGA / OPUS: Container-Check statt nur OggS-Signatur")
}
[[ ${#_missing_tools[@]} -eq 0 ]] && return 0
if ! $terminal ; then
return 0
fi
local _pkg_list="${_missing_pkgs[*]}"
echo ""
echo -e " \033[33mOptionale Validierungs-Tools fehlen – Checks laufen mit reduzierter Genauigkeit:\033[m"
echo ""
local _d
for _d in "${_desc[@]}" ; do
echo " ${_d}"
done
echo ""
echo -n " Jetzt installieren? apt install ${_pkg_list} [j/N]: "
read -r _yn
echo ""
if [[ "$_yn" =~ ^[jJyY]$ ]] ; then
echononl " Installiere Pakete: ${_pkg_list}.."
# shellcheck disable=SC2086
if apt-get install -y ${_missing_pkgs[*]} > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
echo ""
echo " Bitte manuell installieren:"
echo " apt install ${_pkg_list}"
fi
else
echo " Übersprungen — Validierung läuft mit reduzierter Genauigkeit."
fi
echo ""
}
is_number() {
return $(test ! -z "${1##*[!0-9]*}" > /dev/null 2>&1);
@@ -323,37 +396,69 @@ validate_recovered_file() {
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"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded PNG ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode PNG"
fi
else
echo "INVALID|PNG signature missing"
local _head
_head="$(head -c8 "$_f" 2> /dev/null | od -An -tx1 | tr -d ' \n')"
if [[ "$_head" = "89504e470d0a1a0a" ]] ; then
echo "VALID|PNG signature ok (identify not installed — no deep check)"
else
echo "INVALID|PNG signature missing"
fi
fi
;;
gif)
local _head6
_head6="$(head -c6 "$_f" 2> /dev/null)"
if [[ "$_head6" = "GIF87a" || "$_head6" = "GIF89a" ]] ; then
echo "VALID|GIF signature ok"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded GIF ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode GIF"
fi
else
echo "INVALID|GIF signature missing"
local _head6
_head6="$(head -c6 "$_f" 2> /dev/null)"
if [[ "$_head6" = "GIF87a" || "$_head6" = "GIF89a" ]] ; then
echo "VALID|GIF signature ok (identify not installed — no deep check)"
else
echo "INVALID|GIF signature missing"
fi
fi
;;
bmp)
if [[ "$(head -c2 "$_f" 2> /dev/null)" = "BM" ]] ; then
echo "VALID|BMP signature ok"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded BMP ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode BMP"
fi
else
echo "INVALID|BMP signature missing"
if [[ "$(head -c2 "$_f" 2> /dev/null)" = "BM" ]] ; then
echo "VALID|BMP signature ok (identify not installed — no deep check)"
else
echo "INVALID|BMP signature missing"
fi
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"
if command -v identify > /dev/null 2>&1 ; then
if identify -regard-warnings "$_f" > /dev/null 2>&1 ; then
echo "VALID|ImageMagick 'identify' decoded TIFF ok"
else
echo "INVALID|ImageMagick 'identify' failed to decode TIFF"
fi
else
echo "INVALID|TIFF signature missing"
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 (identify not installed — no deep check)"
else
echo "INVALID|TIFF signature missing"
fi
fi
;;
pnm|pgm|ppm|pbm)
@@ -380,22 +485,57 @@ validate_recovered_file() {
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
if unzip -tq "$_f" > /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 "$(unzip -t "$_f" 2>&1 | grep -v '^Archive:' | grep -v '^[[:space:]]*$' | head -1)")"
echo "INVALID|zip integrity check failed${_badentry:+ (${_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"
if command -v ffprobe > /dev/null 2>&1 ; then
local _ffprobe_out _ffprobe_exit
_ffprobe_out="$(ffprobe -v error -show_streams "$_f" 2>&1)"
_ffprobe_exit=$?
if [[ $_ffprobe_exit -eq 0 ]] ; then
local _streams
_streams="$(echo "$_ffprobe_out" | grep -c '\[STREAM\]' || true)"
echo "VALID|ffprobe parsed container ok (${_streams} stream(s) found)"
else
local _ffprobe_err
_ffprobe_err="$(echo "$_ffprobe_out" | head -1)"
echo "INVALID|ffprobe failed to parse container${_ffprobe_err:+ (${_ffprobe_err})}"
fi
else
echo "INVALID|mp4 ftyp box not found"
if head -c 64 "$_f" 2> /dev/null | grep -aq "ftyp" ; then
echo "VALID|mp4 ftyp box found (ffprobe not installed — no deep check)"
else
echo "INVALID|mp4 ftyp box not found"
fi
fi
;;
doc|xls|ppt|ole|msi)
@@ -495,6 +635,72 @@ validate_recovered_file() {
echo "INVALID|GIMP (gimp xcf) signature missing"
fi
;;
mp3)
if command -v mp3val > /dev/null 2>&1 ; then
local _mp3val_out
_mp3val_out="$(mp3val "$_f" 2>&1)"
if echo "$_mp3val_out" | grep -q 'No errors found' ; then
echo "VALID|mp3val: no errors found"
else
local _mp3_err
_mp3_err="$(echo "$_mp3val_out" | grep -iv '^mp3val\|^$' | head -2 | tr '\n' ' ' | sed 's/ $//')"
echo "INVALID|mp3val reported errors${_mp3_err:+ (${_mp3_err})}"
fi
else
local _head3 _head2hex
_head3="$(head -c3 "$_f" 2>/dev/null)"
_head2hex="$(head -c2 "$_f" 2>/dev/null | od -An -tx1 | tr -d ' \n')"
if [[ "$_head3" = "ID3" ]] || echo "$_head2hex" | grep -qE '^fff[bef2]' ; then
echo "VALID|MP3 ID3 tag / MPEG sync ok (mp3val not installed — no deep check)"
else
echo "INVALID|MP3: no ID3 header or MPEG frame sync found"
fi
fi
;;
flac)
if command -v flac > /dev/null 2>&1 ; then
local _flac_out _flac_exit
_flac_out="$(flac --silent --test "$_f" 2>&1)"
_flac_exit=$?
if [[ $_flac_exit -eq 0 ]] ; then
echo "VALID|flac --test: ok"
else
local _flac_err
_flac_err="$(echo "$_flac_out" | grep -v '^$' | tail -1)"
echo "INVALID|flac --test failed${_flac_err:+ (${_flac_err})}"
fi
else
local _head4hex
_head4hex="$(head -c4 "$_f" 2>/dev/null | od -An -tx1 | tr -d ' \n')"
if [[ "$_head4hex" = "664c6143" ]] ; then
echo "VALID|FLAC 'fLaC' signature ok (flac not installed — no deep check)"
else
echo "INVALID|FLAC 'fLaC' signature missing"
fi
fi
;;
ogg|oga|ogv|opus)
if command -v ogginfo > /dev/null 2>&1 ; then
local _ogg_out _ogg_exit
_ogg_out="$(ogginfo "$_f" 2>&1)"
_ogg_exit=$?
if [[ $_ogg_exit -eq 0 ]] ; then
echo "VALID|ogginfo: container parsed ok"
else
local _ogg_err
_ogg_err="$(echo "$_ogg_out" | grep -iv '^Processing\|^$' | head -2 | tr '\n' ' ' | sed 's/ $//')"
echo "INVALID|ogginfo failed${_ogg_err:+ (${_ogg_err})}"
fi
else
local _head4ogg
_head4ogg="$(head -c4 "$_f" 2>/dev/null)"
if [[ "$_head4ogg" = "OggS" ]] ; then
echo "VALID|OGG 'OggS' signature ok (ogginfo not installed — no deep check)"
else
echo "INVALID|OGG 'OggS' signature missing"
fi
fi
;;
wav)
local _riff _wave
_riff="$(head -c4 "$_f" 2> /dev/null)"
@@ -582,6 +788,15 @@ validate_recovered_file() {
## - PDF structural check shared by the 'pdf' case and the 'ai'
## - (PDF-compatible) case. Sets $_pdf_check_detail, returns 0/1.
## -
## - qpdf exit codes:
## - 0 no problems
## - 2 structural errors → file is genuinely corrupt/unreadable
## - 3 warnings only → minor spec non-conformances; every real
## - viewer opens the file without issues
## - Only exit code 2 is treated as INVALID here. Exit code 3 (warnings)
## - is reported as VALID with a note, preventing false positives for
## - real-world PDFs that have trivial non-conformances.
## -
_pdf_check() {
local _f="$1"
if [[ "$(head -c5 "$_f" 2> /dev/null)" != "%PDF-" ]] ; then
@@ -589,11 +804,22 @@ _pdf_check() {
return 1
fi
if command -v qpdf > /dev/null 2>&1 ; then
if qpdf --check "$_f" > /dev/null 2>&1 ; then
local _qpdf_out _qpdf_exit
_qpdf_out="$(qpdf --check "$_f" 2>&1)"
_qpdf_exit=$?
if [[ $_qpdf_exit -eq 0 ]] ; then
_pdf_check_detail="qpdf --check ok"
return 0
elif [[ $_qpdf_exit -eq 3 ]] ; then
# Warnings only — no structural errors. File is readable by all
# standard PDF viewers; non-conformances are minor/cosmetic.
_pdf_check_detail="qpdf --check ok (warnings only — file is readable)"
return 0
else
_pdf_check_detail="qpdf --check failed"
# Exit code 2 (or unexpected): genuine structural errors.
local _first_err
_first_err="$(echo "$_qpdf_out" | grep -i 'error' | head -1 | sed 's/^[[:space:]]*//')"
_pdf_check_detail="qpdf --check failed (exit ${_qpdf_exit}${_first_err:+: ${_first_err}})"
return 1
fi
fi
@@ -761,7 +987,9 @@ 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
# Trap signals AND normal/abnormal exit so that temp files and the
# lock directory are always removed — even on an unexpected crash.
trap 'clean_up 1' SIGHUP SIGINT SIGTERM EXIT
else
@@ -816,7 +1044,6 @@ if ! $_dry_run_explicit && $terminal ; then
but write, back up and verify NOTHING"
echo ""
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
@@ -1123,6 +1350,8 @@ done
mkdir -p "$report_dir" 2> /dev/null
restore_report_file="${report_dir}/restore_${WEBSITE}_${run_date}.tsv"
check_optional_validation_tools
if $terminal ; then
echo ""
if $DRY_RUN ; then
@@ -1164,6 +1393,18 @@ if $terminal ; then
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 "=================================================================="
@@ -1228,6 +1469,13 @@ if ($oldWorkingDir === false) {
}
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) {
@@ -1498,10 +1746,15 @@ for _user in "${selected_user_arr[@]}" ; do
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 $restore_php_file $_user $list_file --dry-run" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file"
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 $restore_php_file $_user $list_file" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file"
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
+26 -1
View File
@@ -582,6 +582,14 @@ fwrite(STDERR, "DEBUG: vor require lib/base.php\n");
require_once __DIR__ . '/lib/base.php';
fwrite(STDERR, "DEBUG: nach require lib/base.php - Bootstrap abgeschlossen\n");
// Nextcloud's own bootstrap (lib/base.php) unconditionally calls
// set_time_limit(3600) as part of require_once 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 a full-content scan of 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) {
@@ -771,6 +779,18 @@ if $terminal ; then
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}/scan_<account>.tsv\033[m"
echo ""
fi
# -----
# - Main part of the script
@@ -810,7 +830,12 @@ for _user in "${selected_user_arr[@]}" ; do
} >> "$report_file"
echononl " Scanning account \033[1;37m${_user}\033[m.."
su -c "$PHP_BIN $scan_php_file $_user" -s /bin/bash $HTTP_USER > "$user_tsv" 2> "$log_file"
# -d max_execution_time=0: a full-content read of every file of a
# large account can easily 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-scan, long
# before any actual error in the account's files.
su -c "$PHP_BIN -d max_execution_time=0 $scan_php_file $_user" -s /bin/bash $HTTP_USER > "$user_tsv" 2> "$log_file"
_rc=$?
if [[ $_rc -ne 0 ]]; then