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