Dirvish Cleanup
Script that will do a dry-run of outdated files to be deleted for Dirvish. Once dry-run is completed and verified, a full run can be completed.
#!/usr/bin/env bash
# cleanup.sh / dirvish_purge_dates.sh
# Delete snapshot directories named YYYY-MM-DD (or loose prefix) under given roots.
# Dry-run preview by default. Use --force to actually delete.
set -euo pipefail
DRY_RUN=1
YES=0
LOOSE=0
YEARS=(2023 2024)
ROOTS=()
EXCLUDES=()
LOGFILE="/var/log/dirvish_purge_dates.log"
usage() {
cat <<'EOF'
Usage: cleanup.sh [options] <ROOT> [ROOT ...]
Options:
-f, --force Actually delete (default is dry-run preview).
-y, --yes Auto-confirm when prompted (still dry-run unless --force).
--loose Match prefix YYYY-* (not strict YYYY-MM-DD).
--years LIST Comma-separated years (e.g. 2021,2022,2023).
--exclude LIST Comma-separated dir-name patterns to skip.
--log FILE Path to log file (default: /var/log/dirvish_purge_dates.log).
-h, --help Show this help.
EOF
}
# -----------------------------
# Parse arguments
# -----------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--force) DRY_RUN=0; shift ;;
-y|--yes) YES=1; shift ;;
--loose) LOOSE=1; shift ;;
--years)
[[ $# -ge 2 ]] || { echo "Error: --years needs a value" >&2; exit 1; }
IFS=',' read -r -a YEARS <<< "$2"
shift 2 ;;
--exclude)
[[ $# -ge 2 ]] || { echo "Error: --exclude needs a value" >&2; exit 1; }
IFS=',' read -r -a EXCLUDES <<< "$2"
shift 2 ;;
--log)
[[ $# -ge 2 ]] || { echo "Error: --log needs a file path" >&2; exit 1; }
LOGFILE="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
*) ROOTS+=("$1"); shift ;;
esac
done
if [[ $# -gt 0 ]]; then ROOTS+=("$@"); fi
if [[ ${#ROOTS[@]} -eq 0 ]]; then
echo "Error: provide at least one ROOT directory." >&2
usage; exit 1
fi
# -----------------------------
# Safety checks
# -----------------------------
for r in "${ROOTS[@]}"; do
[[ -d "$r" ]] || { echo "Error: not a directory: $r" >&2; exit 1; }
[[ "$r" == "/" ]] && { echo "Refusing to operate on root /" >&2; exit 1; }
done
if [[ $DRY_RUN -eq 0 ]]; then
mkdir -p "$(dirname "$LOGFILE")"
touch "$LOGFILE" || { echo "Cannot write log file: $LOGFILE" >&2; exit 1; }
fi
join_alt() { local IFS='|'; echo "${YEARS[*]}"; }
# -----------------------------
# Build exclusion arguments (returns a string)
# -----------------------------
build_exclude_args() {
local args=()
if (( ${#EXCLUDES[@]} )); then
for pat in "${EXCLUDES[@]}"; do
args+=( -not -path "*/${pat}" -not -path "*/${pat}/*" )
done
fi
echo "${args[@]:-}"
}
# -----------------------------
# Build find command as array
# -----------------------------
build_find_cmd() {
local root=$1
local excl_args
excl_args=$(build_exclude_args)
if (( LOOSE )); then
local cmd=(find "$root" -type d "(")
local first=1
for y in "${YEARS[@]}"; do
if (( first )); then
cmd+=( -name "${y}-*" )
first=0
else
cmd+=( -o -name "${y}-*" )
fi
done
cmd+=( ")" )
if [[ -n "$excl_args" ]]; then cmd+=( $excl_args ); fi
cmd+=( -prune -print0 )
printf '%s\0' "${cmd[@]}" | tr '\0' ' '
else
local yrs
yrs=$(join_alt)
local cmd=(find "$root" -regextype posix-extended -type d \
-regex ".*/(${yrs})-[0-9]{2}-[0-9]{2}$" )
if [[ -n "$excl_args" ]]; then cmd+=( $excl_args ); fi
cmd+=( -prune -print0 )
printf '%s\0' "${cmd[@]}" | tr '\0' ' '
fi
}
log_msg() {
[[ $DRY_RUN -eq 0 ]] && echo "[$(date '+%F %T')] $*" >>"$LOGFILE"
}
# -----------------------------
# Main
# -----------------------------
echo "=== $( ((DRY_RUN)) && echo 'DRY RUN' || echo 'DELETION MODE (--force)') ==="
(( ${#EXCLUDES[@]} )) && echo "Excluding patterns: ${EXCLUDES[*]}"
echo "Log file: $LOGFILE"
total=0
for r in "${ROOTS[@]}"; do
echo "Scanning: $r"
cmd_array=($(build_find_cmd "$r"))
count=$("${cmd_array[@]}" | tr -cd '\0' | wc -c | tr -d ' ')
echo " Matches: $count"
total=$(( total + count ))
if (( count > 0 )); then
echo " Preview (up to 30):"
"${cmd_array[@]/-print0/-print}" | head -n 30 | sed 's/^/ /'
if (( DRY_RUN == 0 )); then
echo " Deleting..."
"${cmd_array[@]}" | xargs -0 -I{} sh -c 'echo " removed: {}"; rm -rf -- "{}"'
"${cmd_array[@]}" | xargs -0 -I{} bash -c \
'printf "[%s] removed: %s\n" "$(date "+%F %T")" "{}"' >>"$LOGFILE"
fi
fi
done
echo "Total matches across roots: $total"
if (( DRY_RUN == 1 && total > 0 )); then
echo
if (( YES == 0 )); then
read -r -p "Type DELETE to remove these directories now (or Enter to abort): " ans
[[ "$ans" == "DELETE" ]] || { echo "Aborted. Nothing deleted."; exit 0; }
fi
exec_args=( --force )
(( LOOSE )) && exec_args+=( --loose )
exec_args+=( --years "$(IFS=','; echo "${YEARS[*]}")" )
(( ${#EXCLUDES[@]} )) && exec_args+=( --exclude "$(IFS=','; echo "${EXCLUDES[*]}")" )
exec_args+=( --log "$LOGFILE" )
exec_args+=( "${ROOTS[@]}" )
exec "$0" "${exec_args[@]}"
else
(( DRY_RUN )) && echo "Dry-run complete. Nothing deleted."
fi