992 lines
30 KiB
Bash
Executable File
992 lines
30 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"
|
|
|
|
# - 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 <website> -f <pathlist-file>
|
|
|
|
\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' - '/<uid>/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}/<website>,
|
|
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 <website>
|
|
The site of the nextcloud instance.
|
|
|
|
-f <pathlist-file>
|
|
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
|
|
# --- ('/<uid>/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 '/<uid>/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 '/<uid>/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 '/<uid>/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'
|
|
<?php
|
|
/**
|
|
* delete_files.php
|
|
*
|
|
* Deletes an explicit list of files from Nextcloud's own storage, at
|
|
* their ORIGINAL path, through the normal Files API ($node->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 <uid> <listFile> [--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 <uid> <listFile> [--dry-run]\n");
|
|
exit(1);
|
|
}
|
|
if (!is_file($listFile)) {
|
|
fwrite(STDERR, "Liste nicht gefunden: $listFile\n");
|
|
exit(1);
|
|
}
|
|
|
|
$rootFolder = \OC::$server->get(\OCP\Files\IRootFolder::class);
|
|
\OC_Util::setupFS($uid);
|
|
$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
|