#!/usr/bin/env bash # retoor set -uo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null || true REPO_DIR="${DEVPLACE_REPO_DIR:-/home/retoor/projects/devplacepy}" cd "$REPO_DIR" || { echo "Cannot cd into $REPO_DIR - set DEVPLACE_REPO_DIR"; exit 1; } if [ -x "$REPO_DIR/.venv/bin/devplace" ]; then DEVPLACE="$REPO_DIR/.venv/bin/devplace" elif command -v devplace >/dev/null 2>&1; then DEVPLACE="devplace" else echo "Cannot find the devplace binary (looked in $REPO_DIR/.venv/bin and PATH)." echo "Run 'make install' once to create the .venv, or activate it yourself." exit 1 fi if [ -x "$REPO_DIR/.venv/bin/python" ]; then PYTHON="$REPO_DIR/.venv/bin/python" elif command -v python3 >/dev/null 2>&1; then PYTHON="python3" else echo "Cannot find a python interpreter (looked in $REPO_DIR/.venv/bin and PATH)." exit 1 fi DIRS=( data/uploads data/uploads/attachments data/uploads/project_files data/zips data/zip_staging data/fork_staging data/seo_reports data/deepsearch data/isslop data/container_workspaces data/workspace_state ) show_sizes() { echo "--- $1 ---" for d in "${DIRS[@]}"; do if [ -d "$d" ]; then printf " %-10s %s\n" "$(du -sh "$d" 2>/dev/null | cut -f1)" "$d" else printf " %-10s %s\n" "-" "$d (absent)" fi done echo } FAILED=() run() { echo "=== devplace $* ===" "$DEVPLACE" "$@" local status=$? if [ "$status" -ne 0 ]; then echo "!!! FAILED (exit $status): devplace $*" FAILED+=("devplace $*") fi echo } echo "########################################" echo "# DevPlace maintenance cleanup" echo "# backups and news are deliberately excluded" echo "########################################" echo show_sizes "BEFORE" run attachments prune run zips prune run zips clear run forks prune run forks clear run seo prune run seo clear run seo-meta prune run seo-meta clear run deepsearch prune run deepsearch clear run isslop prune run isslop clear run quiz prune run game market prune run game steals prune run messaging prune-tickets run devii tasks prune run accounts prune run containers prune run containers prune-builds run containers gc-workspaces show_sizes "AFTER" echo "########################################" echo "# Soft-deleted attachment / project-file blobs still on disk" echo "# (READ-ONLY report - nothing below this line deletes anything;" echo "# none of the commands above purge these, only the admin Trash" echo "# 'Purge' button per-event, or accounts prune, do that today)" echo "########################################" "$PYTHON" - "$REPO_DIR" <<'PY' import sqlite3 import sys from pathlib import Path repo_dir = Path(sys.argv[1]) data_dir = Path(__import__("os").environ.get("DEVPLACE_DATA_DIR", str(repo_dir / "data"))) db_path = data_dir / "devplace.db" def human(n): n = float(n) for unit in ("B", "KB", "MB", "GB", "TB"): if n < 1024: return f"{n:.1f}{unit}" n /= 1024 return f"{n:.1f}PB" if not db_path.exists(): print(f" {db_path} does not exist, skipping") raise SystemExit(0) con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) try: cur = con.cursor() def columns(table): return {row[1] for row in cur.execute(f"PRAGMA table_info('{table}')")} # attachments: data/uploads/attachments/{directory}/{stored_name}, plus any # {stem}_thumb.* sibling thumbnail (see attachments._unlink_attachment_files) cols = columns("attachments") if {"directory", "stored_name", "deleted_at"} <= cols: rows = cur.execute( "SELECT directory, stored_name FROM attachments WHERE deleted_at IS NOT NULL" ).fetchall() base = data_dir / "uploads" / "attachments" present, total = 0, 0 for directory, stored_name in rows: if not directory or not stored_name: continue fp = base / directory / stored_name if fp.exists(): present += 1 try: total += fp.stat().st_size except OSError: pass stem = Path(stored_name).stem for thumb in (base / directory).glob(f"{stem}_thumb.*"): try: total += thumb.stat().st_size except OSError: pass print( f" attachments: {len(rows)} soft-deleted row(s), " f"{present} still on disk, {human(total)} reclaimable" ) else: print(f" attachments: unexpected schema, columns={sorted(cols)}") # project_files: data/uploads/project_files/{directory}/{stored_name}, # only rows with is_binary=1 have a blob at all (text lives in the DB row) cols = columns("project_files") if {"directory", "stored_name", "deleted_at", "is_binary"} <= cols: rows = cur.execute( "SELECT directory, stored_name FROM project_files " "WHERE deleted_at IS NOT NULL AND is_binary = 1" ).fetchall() base = data_dir / "uploads" / "project_files" present, total = 0, 0 for directory, stored_name in rows: if not directory or not stored_name: continue fp = base / directory / stored_name if fp.exists(): present += 1 try: total += fp.stat().st_size except OSError: pass print( f" project_files: {len(rows)} soft-deleted binary row(s), " f"{present} still on disk, {human(total)} reclaimable" ) else: print(f" project_files: unexpected schema, columns={sorted(cols)}") finally: con.close() PY echo if [ "${#FAILED[@]}" -gt 0 ]; then echo "########################################" echo "# ${#FAILED[@]} command(s) failed:" for cmd in "${FAILED[@]}"; do echo "# $cmd" done echo "########################################" exit 1 fi echo "All commands completed successfully."