Files
devplacepy/run_maintenance_cleanup.sh
T

211 lines
6.0 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# retoor <retoor@molodetz.nl>
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."