#!/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" # - Before this script deletes a file from Nextcloud's live storage, it # - keeps a raw, byte-for-byte copy of the CURRENT on-disk ciphertext # - (the file exactly as it is right now) in a separate directory - a # - plain filesystem copy, independent of Nextcloud/its Trash entirely. # - This is an extra safety net on top of (not a replacement for) # - Nextcloud's own Trash, which is where a normal delete through the # - Files API already sends the file if the 'files_trashbin' app is # - enabled. # - DEFAULT_DELETE_BACKUP_BASE_DIR="/var/nc-delete-backup" declare -a unsorted_website_arr declare -a website_arr declare -a unsorted_account_arr declare -a account_arr LOCK_DIR="/tmp/${script_name%%.*}.LOCK" log_file="${LOCK_DIR}/${script_name%%.*}.log" run_date=$(date +%Y-%m-%d-%H%M) # ============= # --- Some functions # ============= usage() { [[ -n "$1" ]] && error "$1" [[ $terminal ]] && echo -e " \033[1mUsage:\033[m $(basename $0) -s -f \033[1mDescription\033[m Generic, reusable companion to restore_bad_signature.sh: deletes an explicit, hand-picked list of files from a Nextcloud instance's live storage, through Nextcloud's normal Files API - the same delete path the web interface, WebDAV, or a sync client uses. If the 'files_trashbin' app is enabled (the default), the file ends up in that account's Trash, not gone outright. This is a plain, general-purpose tool - NOT specific to any one account, site, or incident. It is meant for the second half of the 'bad signature' recovery workflow: after recover_bad_signature.sh and restore_bad_signature.sh have taken care of every file that validates cleanly or at least plausibly, a handful of files sometimes remain that a human should look at and decide on one by one (system/index files that don't belong in the cloud at all, fragments from a browser 'save page' action, or anything else you simply don't want kept). Once you've decided, list their paths in a plain text file and point this script at it. IMPORTANT: - The path list file (-f) is plain text, one Nextcloud path per line, in the SAME format used throughout this toolkit's own reports (the 'path' column, e.g. '/inge/files/Some/Folder/file.ext' - '//files/...'). Blank lines and lines starting with '#' are ignored. The account (uid) is taken from each path's own first segment, so one list can freely mix files from several accounts on the same site - no separate account selection is needed. - Nothing is guessed or auto-selected: every path this script acts on is one YOU explicitly listed. There is no scanning, no heuristic, no 'delete everything that looks like junk' mode here - that judgment call happens before this script runs, not inside it. - Before each file is deleted (unless dry-run), the CURRENT on-disk ciphertext is copied byte-for-byte to a separate backup directory (default ${DEFAULT_DELETE_BACKUP_BASE_DIR}/, override with DELETE_BACKUP_BASE_DIR in the conf file) - a plain filesystem copy that bypasses Nextcloud/encryption and Trash entirely. This only works for local/default storage; it is best-effort and a missing source is reported but does not abort the run. - The delete itself goes through Nextcloud's normal Files API (\$node->delete()), so all of Nextcloud's own rules still apply: if 'files_trashbin' is enabled the file is moved to that account's Trash (recoverable there, subject to the instance's own Trash retention settings); if it is disabled, the delete is immediate and permanent. - A path already absent (no matching file found) is reported as NOT_FOUND, not as an error - nothing to do there. Unless '-n' is given on the command line, you will be asked interactively whether to run a dry-run (nothing is touched, just reports what would happen) or the real thing. \033[1mOptions\033[m -s The site of the nextcloud instance. -f Plain text file with one Nextcloud path to delete per line (see above). Required - if omitted, you will be asked for it. -n Dry-run: goes through selection and reporting, but does NOT back up or delete anything. Passing it here skips the interactive mode question mentioned above. \033[1mExample:\033[m Runs this script on system 'cloud-01.oopen.de' $(basename $0) -s cloud-01.oopen.de -f /root/to_delete.txt Dry-run only, nothing is deleted: $(basename $0) -n -s cloud-01.oopen.de -f /root/to_delete.txt " clean_up 1 } clean_up() { # Perform program exit housekeeping [[ -n "$delete_php_file" ]] && rm -f "$delete_php_file" 2> /dev/null rm -rf "$LOCK_DIR" blank_line exit $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" } ## - 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 } blank_line() { if $terminal ; then echo "" fi } # - Running in a terminal? # - if [[ -t 1 ]] ; then terminal=true else terminal=false fi # - This script needs root privileges (reads the Nextcloud data # - directory for the raw ciphertext backup, 'su' into the webserver # - user to perform the actual delete). # - if [[ "$(id -u)" -ne 0 ]] ; then fatal "This script must be run as root (it needs to read the Nextcloud data directory for backups and 'su' into the webserver user). Please re-run as root, e.g. via sudo." fi # ---------- # - Jobhandling # ---------- if pgrep -f "$(basename $0)" | grep -q -v $$ ; then msg="A previos instance of script \"`basename $0`\" seems already be running." echo "" if $terminal ; then echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg" echo "" echo -e " \033[31m\033[1mScript was interupted\033[m!" else echo " [ Fatal ]: $msg" echo "" echo " Script was interupted!" fi echo exit 1 else if [[ -d "$LOCK_DIR" ]] ; then rm -rf "$LOCK_DIR" 2> /dev/null fi fi # - If job already runs, stop execution.. # - if mkdir "$LOCK_DIR" 2> /dev/null ; then # - Remove lockdir when the script finishes, or when it receives a signal # - trap clean_up SIGHUP SIGINT SIGTERM else msg="A previos instance of script \"`basename $0`\" seems already be running." echo "" if $terminal ; then echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg" echo "" echo -e " \033[31m\033[1mScript was interupted\033[m!" else echo " [ Fatal ]: $msg" echo "" echo " Script was interupted!" fi echo exit 1 fi # ------------- # - Read in Commandline arguments # ------------- DRY_RUN=false _dry_run_explicit=false PATHLIST_FILE="" while getopts hns:f: opt ; do case $opt in h) usage ;; n) DRY_RUN=true; _dry_run_explicit=true ;; s) WEBSITE=$OPTARG ;; f) PATHLIST_FILE=$OPTARG ;; \?) usage esac done # ============= # --- Ask which mode to run in, unless '-n' was already given on the # --- command line. The SAFE choice (dry-run) is the default here on # --- purpose - this script deletes from live Nextcloud storage. # ============= if ! $_dry_run_explicit && $terminal ; then blank_line echo -e "\033[37m\033[1mWhich mode should this run use?\033[m" echo "" echo -e " \033[1m[1] Dry-run\033[m - go through the list and report what would happen, but touch NOTHING" echo "" echo " [2] Real delete - actually back up and delete each listed file in Nextcloud" info "Just press Return to use the default: [1] Dry-run." echo -n " Select mode by number [1]: " read _mode_choice case "$(trim "$_mode_choice")" in 2) DRY_RUN=false ;; ""|1) DRY_RUN=true ;; *) fatal "Invalid selection '$_mode_choice'." ;; esac fi if [[ -z "$PATHLIST_FILE" ]] ; then blank_line echo -n " Path to the file listing the Nextcloud paths to delete (-f): " read PATHLIST_FILE fi PATHLIST_FILE="$(trim "$PATHLIST_FILE")" if [[ -z "$PATHLIST_FILE" ]] ; then fatal "No path list file given." fi if [[ ! -f "$PATHLIST_FILE" ]] ; then fatal "Path list file '$PATHLIST_FILE' not found." 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)" [[ -n "$DELETE_BACKUP_BASE_DIR" ]] || DELETE_BACKUP_BASE_DIR=$DEFAULT_DELETE_BACKUP_BASE_DIR backup_dir="${DELETE_BACKUP_BASE_DIR}/${WEBSITE}" # ============= # --- Some # ============= # - Support systemd ? # - SYSTEMD_EXISTS=false systemd=$(which systemd) systemctl=$(which systemctl) if [[ -n "$systemd" ]] || [[ -n "$systemctl" ]] ; then SYSTEMD_EXISTS=true fi if $terminal ; then echo "" echo -e "\033[32m-----\033[m" echo -e "Delete an explicit, hand-picked list of files from system \033[1m${WEB_BASE_DIR}\033[m" echo -e "\033[1m This deletes from Nextcloud's live storage (goes to that account's Trash if 'files_trashbin' is enabled). Only the exact paths listed in '${PATHLIST_FILE}' are ever touched - nothing is scanned, guessed or auto-selected. The current on-disk ciphertext is backed up first, independent of Nextcloud/Trash entirely.\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 # ============= # --- Read and parse the path list file: one Nextcloud path per line, # --- blank lines and '#' comments ignored. The account (uid) for each # --- path is taken from the path's own first segment # --- ('//files/...'), so one list can span several accounts. # ============= parsed_entries_file="${LOCK_DIR}/parsed_entries.tsv" declare -i total_malformed=0 declare -a malformed_lines=() > "$parsed_entries_file" while IFS= read -r _line || [[ -n "$_line" ]] ; do _line="$(trim "$_line")" [[ -z "$_line" ]] && continue [[ "$_line" == \#* ]] && continue if [[ "$_line" =~ ^/([^/]+)/files/ ]] ; then _uid="${BASH_REMATCH[1]}" printf '%s\t%s\n' "$_uid" "$_line" >> "$parsed_entries_file" else (( total_malformed++ )) malformed_lines+=("$_line") fi done < "$PATHLIST_FILE" if [[ ! -s "$parsed_entries_file" ]] ; then fatal "No usable paths found in '$PATHLIST_FILE' (expected one '//files/...' path per line)." fi unsorted_account_arr=($(cut -f1 "$parsed_entries_file" | sort -u)) IFS=$'\n' account_arr=($(sort <<<"${unsorted_account_arr[*]}")) IFS=$CUR_IFS blank_line if $terminal ; then echo -e "\033[37m\033[1mAccounts found in '$(basename "$PATHLIST_FILE")'\033[m" echo "" for _a in "${account_arr[@]}" ; do _a_count=$(awk -F'\t' -v u="$_a" '$1==u' "$parsed_entries_file" | wc -l) echo " - $_a (${_a_count})" done echo "" if [[ $total_malformed -gt 0 ]] ; then warn "${total_malformed} line(s) in '$PATHLIST_FILE' did not look like a '//files/...' path and were IGNORED:" for _l in "${malformed_lines[@]}" ; do echo " $_l" done fi fi # ============= # --- Confirmation # ============= mkdir -p "$report_dir" 2> /dev/null delete_report_file="${report_dir}/delete_${WEBSITE}_${run_date}.tsv" if $terminal ; then echo "" if $DRY_RUN ; then echo -e "\033[1;32mStarting DRY-RUN delete for \033[1;37m${WEBSITE}\033[m" else echo -e "\033[1;31m\033[1mStarting REAL delete (removes from live Nextcloud storage) for \033[1;37m${WEBSITE}\033[m" fi echo "" echo -e " Cloud instance..........................: $WEBSITE" echo -e " Path list file...........................: $PATHLIST_FILE" echo -e " Accounts affected........................: \033[33m${account_arr[*]}\033[m" echo "" if $DRY_RUN ; then echo -e " Nothing will be touched - dry-run only." else echo -e " Ciphertext backup (before delete)........: $backup_dir" echo -e " Files deleted at their ORIGINAL path in Nextcloud (-> Trash, if enabled)." fi echo -e " Delete report.............................: reports/$(basename "${delete_report_file}")" echo "" if $DRY_RUN ; then info "Dry-run: nothing is backed up or deleted." else warn "This DELETES from Nextcloud's live storage (through the normal Files API - goes to Trash if 'files_trashbin' is enabled). Only the exact paths listed in '${PATHLIST_FILE}' are touched. The current on-disk ciphertext is copied byte-for-byte to '${backup_dir}' first, independent of Nextcloud/Trash entirely." 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 delete the listed files for each affected account on \033[1;37m$WEBSITE \033[m" else fatal "Abort by user request - Answer as not 'YES'" fi fi { echo "==================================================================" echo " Delete-Versuch (Liste) ${WEBSITE} ${run_date}" echo "==================================================================" echo "" echo " Pfadliste: $(basename "$PATHLIST_FILE")" echo " Nur die dort explizit aufgefuehrten Pfade werden geloescht." $DRY_RUN && echo " DRY-RUN: es wurde NICHTS gesichert oder geloescht." ! $DRY_RUN && echo " Chiffretext-Sicherung (vor dem Loeschen) liegt unter: ${backup_dir}" [[ $total_malformed -gt 0 ]] && echo " ${total_malformed} Zeile(n) aus der Pfadliste wurden ignoriert (kein '//files/...'-Pfad)." echo "" echo -e "path\tstatus\tdetail" } > "$delete_report_file" # ============= # --- Write the embedded PHP delete script # ============= delete_php_file="${INSTALL_DIR}/.delete_files_$$.php" cat > "$delete_php_file" <<'PHP_DELETE_EOF' delete()) * - the same delete path the web interface, WebDAV, or a sync client * uses. If the 'files_trashbin' app is enabled, Nextcloud moves the * file to that account's Trash itself; this script does not bypass * that. * * Usage: php delete_files.php [--dry-run] * listFile: one Nextcloud path per line (no header, no other columns) */ error_reporting(E_ALL); ini_set('display_errors', '1'); register_shutdown_function(function () { $err = error_get_last(); if ($err !== null && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) { fwrite(STDERR, "DEBUG: FATAL bei Shutdown: [{$err['type']}] {$err['message']} in {$err['file']}:{$err['line']}\n"); } }); define('OC_CONSOLE', 1); $oldWorkingDir = getcwd(); if ($oldWorkingDir === false) { fwrite(STDERR, "Konnte aktuelles Arbeitsverzeichnis nicht ermitteln. Bitte mit absolutem Pfad aufrufen.\n"); exit(1); } chdir(__DIR__); require_once __DIR__ . '/lib/base.php'; chdir($oldWorkingDir); if (function_exists('posix_getuid') && posix_getuid() === 0) { fwrite(STDERR, "Bitte NICHT als root ausfuehren, sondern als Webserver-User.\n"); exit(1); } try { $uid = $argv[1] ?? null; $listFile = $argv[2] ?? null; $dryRun = in_array('--dry-run', $argv, true); if ($uid === null || $listFile === null) { fwrite(STDERR, "Usage: php delete_files.php [--dry-run]\n"); exit(1); } if (!is_file($listFile)) { fwrite(STDERR, "Liste nicht gefunden: $listFile\n"); exit(1); } $rootFolder = \OC::$server->get(\OCP\Files\IRootFolder::class); \OC_Util::setupFS($uid); $userFolder = $rootFolder->getUserFolder($uid); $lines = file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if ($lines === false) { $lines = []; } $total = 0; $deleted = 0; $notFound = 0; $failed = 0; echo "path\tstatus\tdetail\n"; foreach ($lines as $path) { $path = trim($path); if ($path === '') { continue; } $total++; try { $node = null; try { $node = $rootFolder->get($path); } catch (\Throwable $e) { $node = null; } if ($node === null) { $notFound++; echo "$path\tNOT_FOUND\talready absent - nothing to delete\n"; flush(); continue; } if ($dryRun) { $deleted++; $size = ($node instanceof \OCP\Files\File) ? $node->getSize() : 0; echo "$path\tDRY_RUN\twould delete now (${size} bytes) - goes to Trash if 'files_trashbin' is enabled\n"; flush(); continue; } $node->delete(); // Verify: the normal read path should no longer find it // (it may still exist in Trash - that is Nextcloud's own // behaviour, not a failure of this delete). clearstatcache(); $stillThere = null; try { $stillThere = $rootFolder->get($path); } catch (\Throwable $e) { $stillThere = null; } if ($stillThere === null) { $deleted++; echo "$path\tOK\tdeleted (moved to Trash, if 'files_trashbin' is enabled)\n"; } else { $failed++; echo "$path\tDELETE_ERROR\tdelete() did not throw, but the path is still resolvable afterwards\n"; } flush(); } catch (\Throwable $e) { $failed++; $msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage()); echo "$path\tDELETE_ERROR\t" . get_class($e) . ": $msg\n"; flush(); } } fwrite(STDERR, "\nFertig. Dateien verarbeitet: $total, geloescht: $deleted, nicht gefunden: $notFound, 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_DELETE_EOF chmod 644 "$delete_php_file" if ! $DRY_RUN ; then mkdir -p "$backup_dir" 2> /dev/null fi declare -i total_selected=0 declare -i total_ok=0 declare -i total_not_found=0 declare -i total_errors=0 declare -i total_failed_users=0 for _user in "${account_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" } >> "$delete_report_file" declare -i _user_selected=0 list_file="${LOCK_DIR}/list_${_user}.tsv" > "$list_file" while IFS=$'\t' read -r _u _path ; do [[ "$_u" != "$_user" ]] && continue (( _user_selected++ )) if ! $DRY_RUN ; then _rel="${_path#/${_user}/files/}" _orig_ciphertext="${DATA_DIR}/${_user}/files/${_rel}" if [[ -f "$_orig_ciphertext" ]] ; then _backup_target="${backup_dir}/${_user}/${_rel}" mkdir -p "$(dirname "$_backup_target")" 2> /dev/null cp -p "$_orig_ciphertext" "$_backup_target" 2> /dev/null fi fi printf '%s\n' "$_path" >> "$list_file" done < "$parsed_entries_file" total_selected+=$_user_selected user_result_tsv="${LOCK_DIR}/result_${_user}.tsv" if $DRY_RUN ; then echononl " Dry-run for account \033[1;37m${_user}\033[m (${_user_selected} to check).." else echononl " Deleting for account \033[1;37m${_user}\033[m (${_user_selected} to delete).." fi if [[ -s "$list_file" ]] ; then if $DRY_RUN ; then su -c "$PHP_BIN $delete_php_file $_user $list_file --dry-run" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file" else su -c "$PHP_BIN $delete_php_file $_user $list_file" -s /bin/bash $HTTP_USER > "$user_result_tsv" 2> "$log_file" fi _rc=$? else echo -e "path\tstatus\tdetail" > "$user_result_tsv" _rc=0 fi if [[ $_rc -ne 0 ]]; then echo_failed error "$(cat "$log_file")" echo " [ FEHLER beim Delete-Lauf - siehe Server-Log ]" >> "$delete_report_file" (( total_failed_users++ )) unset _user_selected continue fi echo_done $terminal && echo "" declare -i _user_ok=0 declare -i _user_not_found=0 declare -i _user_errors=0 declare -a _user_error_lines=() while IFS=$'\t' read -r _path _status _detail ; do [[ "$_path" = "path" ]] && continue [[ -z "$_path" ]] && continue case "$_status" in OK|DRY_RUN) (( _user_ok++ )) ;; NOT_FOUND) (( _user_not_found++ )) ;; *) (( _user_errors++ )); _user_error_lines+=("${_path}"$'\t'"${_status}: ${_detail}") ;; esac printf '%s\t%s\t%s\n' "$_path" "$_status" "$_detail" >> "$delete_report_file" done < "$user_result_tsv" _user_pct_ok="$(calc_percent "$_user_ok" "$_user_selected")" if $terminal ; then echo -e " \033[1;37mAccount ${_user}\033[m" echo -e " Ausgewaehlt (aus der Pfadliste)............: ${_user_selected}" if $DRY_RUN ; then echo -e " Wuerde geloescht werden.....................: \033[1;32m${_user_ok} (${_user_pct_ok} %)\033[m" else if [[ $_user_ok -gt 0 ]] ; then echo -e " Geloescht....................................: \033[1;32m${_user_ok} (${_user_pct_ok} %)\033[m" else echo -e " Geloescht....................................: ${_user_ok} (${_user_pct_ok} %)" fi fi if [[ $_user_not_found -gt 0 ]] ; then echo -e " Bereits nicht mehr vorhanden................: ${_user_not_found}" fi if [[ $_user_errors -gt 0 ]] ; then echo -e " Fehler beim Loeschen.........................: \033[1;31m${_user_errors}\033[m" fi echo "" fi { if [[ $_user_errors -gt 0 ]] ; then echo "" echo " ------------------------------------------------------------------" echo " Fehler beim Loeschen - Account ${_user} (${_user_errors})" echo " ------------------------------------------------------------------" for _line in "${_user_error_lines[@]}" ; do printf ' %s\n' "$_line" done fi } >> "$delete_report_file" { echo "" echo " Account ${_user}" echo " Ausgewaehlt (aus der Pfadliste)............: ${_user_selected}" if $DRY_RUN ; then echo " Wuerde geloescht werden.....................: ${_user_ok} (${_user_pct_ok} %)" else echo " Geloescht....................................: ${_user_ok} (${_user_pct_ok} %)" fi [[ $_user_not_found -gt 0 ]] && echo " Bereits nicht mehr vorhanden................: ${_user_not_found}" [[ $_user_errors -gt 0 ]] && echo " Fehler beim Loeschen.........................: ${_user_errors}" } >> "$delete_report_file" (( total_ok += _user_ok )) (( total_not_found += _user_not_found )) (( total_errors += _user_errors )) unset _user_selected _user_ok _user_not_found _user_errors _user_error_lines done total_pct_ok="$(calc_percent "$total_ok" "$total_selected")" { echo "" echo "" echo "==================================================================" echo " Gesamtergebnis" echo "==================================================================" echo "" echo "Betroffene Accounts..........................: ${#account_arr[@]}" [[ $total_failed_users -gt 0 ]] && echo "Accounts mit Delete-Fehler...................: ${total_failed_users}" echo "Ausgewaehlt (aus der Pfadliste)..............: ${total_selected}" if $DRY_RUN ; then echo "Wuerde geloescht werden insgesamt............: ${total_ok} (${total_pct_ok} %)" else echo "Geloescht insgesamt...........................: ${total_ok} (${total_pct_ok} %)" fi [[ $total_not_found -gt 0 ]] && echo "Bereits nicht mehr vorhanden insgesamt.......: ${total_not_found}" [[ $total_errors -gt 0 ]] && echo "Fehler beim Loeschen insgesamt................: ${total_errors}" echo "" if $DRY_RUN ; then echo "DRY-RUN: es wurde nichts gesichert oder geloescht." else echo "Chiffretext-Sicherung (vor dem Loeschen) liegt unter: ${backup_dir}" fi } >> "$delete_report_file" blank_line if $terminal ; then echo -e "\033[37m\033[1mErgebnis\033[m" echo "" echo -e " Betroffene Accounts..........................: ${#account_arr[@]}" [[ $total_failed_users -gt 0 ]] && echo -e " Accounts mit Delete-Fehler...................: \033[1;31m${total_failed_users}\033[m" echo -e " Ausgewaehlt (aus der Pfadliste)..............: ${total_selected}" if $DRY_RUN ; then echo -e " Wuerde geloescht werden insgesamt............: \033[1;32m${total_ok} (${total_pct_ok} %)\033[m" else if [[ $total_ok -gt 0 ]] ; then echo -e " Geloescht insgesamt...........................: \033[1;32m${total_ok} (${total_pct_ok} %)\033[m" else echo -e " Geloescht insgesamt...........................: ${total_ok} (${total_pct_ok} %)" fi fi [[ $total_not_found -gt 0 ]] && echo -e " Bereits nicht mehr vorhanden insgesamt.......: ${total_not_found}" [[ $total_errors -gt 0 ]] && echo -e " Fehler beim Loeschen insgesamt................: \033[1;31m${total_errors}\033[m" echo "" echo -e " Delete-Report.................................: reports/$(basename "${delete_report_file}")" if ! $DRY_RUN ; then echo -e " Chiffretext-Sicherung.........................: $backup_dir" fi echo "" if $DRY_RUN ; then info "Dry-run beendet - es wurde nichts geloescht. Ohne '-n' (oder mit [2] bei der Modusabfrage) fuer den echten Delete erneut ausfuehren." else warn "Bitte pruefen, ob die geloeschten Dateien wie erwartet im Trash der jeweiligen Accounts liegen (sofern 'files_trashbin' aktiviert ist)." fi fi clean_up 0