211 lines
6.0 KiB
Bash
211 lines
6.0 KiB
Bash
#!/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."
|