Big changes
This commit is contained in:
parent
4240ad38e2
commit
e3f0d63fec
99 changed files with 8804 additions and 1731 deletions
63
scripts/db/backup_full.sh
Executable file
63
scripts/db/backup_full.sh
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
project_root="$(cd "${script_dir}/../.." && pwd)"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/db/backup_full.sh [--plain]
|
||||
|
||||
Creates a PostgreSQL logical backup from the running `db` compose service.
|
||||
|
||||
Options:
|
||||
--plain Write a plain SQL dump instead of custom-format dump.
|
||||
-h, --help Show this help.
|
||||
|
||||
Environment:
|
||||
BACKUP_DIR Destination directory (default: <repo>/backups)
|
||||
BACKUP_PREFIX File prefix (default: scholarr)
|
||||
USE_DEV_COMPOSE Set to 1 to include docker-compose.dev.yml
|
||||
USAGE
|
||||
}
|
||||
|
||||
format="custom"
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--plain)
|
||||
format="plain"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
compose_cmd=(docker compose -f "${project_root}/docker-compose.yml")
|
||||
if [[ "${USE_DEV_COMPOSE:-0}" == "1" && -f "${project_root}/docker-compose.dev.yml" ]]; then
|
||||
compose_cmd+=(-f "${project_root}/docker-compose.dev.yml")
|
||||
fi
|
||||
|
||||
backup_dir="${BACKUP_DIR:-${project_root}/backups}"
|
||||
backup_prefix="${BACKUP_PREFIX:-scholarr}"
|
||||
mkdir -p "${backup_dir}"
|
||||
|
||||
timestamp="$(date -u +"%Y%m%dT%H%M%SZ")"
|
||||
if [[ "${format}" == "plain" ]]; then
|
||||
backup_file="${backup_dir}/${backup_prefix}_${timestamp}.sql"
|
||||
dump_cmd='pg_dump --no-owner --no-acl -U "$POSTGRES_USER" "$POSTGRES_DB"'
|
||||
else
|
||||
backup_file="${backup_dir}/${backup_prefix}_${timestamp}.dump"
|
||||
dump_cmd='pg_dump --format=custom --no-owner --no-acl -U "$POSTGRES_USER" "$POSTGRES_DB"'
|
||||
fi
|
||||
|
||||
"${compose_cmd[@]}" exec -T db sh -lc "${dump_cmd}" > "${backup_file}"
|
||||
|
||||
echo "Backup created: ${backup_file}"
|
||||
50
scripts/db/check_integrity.py
Executable file
50
scripts/db/check_integrity.py
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from app.db.session import get_session_factory
|
||||
from app.services.domains.dbops import collect_integrity_report
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Run database integrity checks.")
|
||||
parser.add_argument(
|
||||
"--strict-warnings",
|
||||
action="store_true",
|
||||
help="Return non-zero exit code if any warning is present.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
async def _run() -> dict:
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as db_session:
|
||||
return await collect_integrity_report(db_session)
|
||||
|
||||
|
||||
def _exit_code(report: dict, *, strict_warnings: bool) -> int:
|
||||
if report.get("status") == "failed":
|
||||
return 1
|
||||
if strict_warnings and report.get("warnings"):
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = build_parser().parse_args()
|
||||
|
||||
try:
|
||||
report = asyncio.run(_run())
|
||||
except Exception as exc:
|
||||
print(json.dumps({"status": "failed", "error": str(exc)}, indent=2))
|
||||
return 1
|
||||
|
||||
print(json.dumps(report, indent=2))
|
||||
return _exit_code(report, strict_warnings=args.strict_warnings)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
66
scripts/db/repair_publication_links.py
Executable file
66
scripts/db/repair_publication_links.py
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from app.db.session import get_session_factory
|
||||
from app.services.domains.dbops import run_publication_link_repair
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Repair scholar-publication links for a user with audit logging.",
|
||||
)
|
||||
parser.add_argument("--user-id", type=int, required=True, help="Target user ID.")
|
||||
parser.add_argument(
|
||||
"--scholar-profile-id",
|
||||
type=int,
|
||||
action="append",
|
||||
default=[],
|
||||
help="Optional scholar_profile_id filter. Repeat for multiple values.",
|
||||
)
|
||||
parser.add_argument("--apply", action="store_true", help="Apply changes. Default is dry-run.")
|
||||
parser.add_argument(
|
||||
"--gc-orphan-publications",
|
||||
action="store_true",
|
||||
help="Delete publications with zero links after cleanup.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--requested-by",
|
||||
default="",
|
||||
help="Operator identifier for audit logs (email/name/ticket).",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
async def _run(args: argparse.Namespace) -> dict:
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as db_session:
|
||||
return await run_publication_link_repair(
|
||||
db_session,
|
||||
user_id=args.user_id,
|
||||
scholar_profile_ids=args.scholar_profile_id,
|
||||
dry_run=not args.apply,
|
||||
gc_orphan_publications=args.gc_orphan_publications,
|
||||
requested_by=args.requested_by,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
result = asyncio.run(_run(args))
|
||||
except Exception as exc:
|
||||
print(json.dumps({"status": "failed", "error": str(exc)}, indent=2))
|
||||
return 1
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
79
scripts/db/restore_dump.sh
Executable file
79
scripts/db/restore_dump.sh
Executable file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
project_root="$(cd "${script_dir}/../.." && pwd)"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/db/restore_dump.sh --file <path> [--wipe-public]
|
||||
|
||||
Restores a PostgreSQL dump into the running `db` compose service.
|
||||
|
||||
Options:
|
||||
--file <path> Required. Backup file (.dump custom format or .sql plain).
|
||||
--wipe-public Drop and recreate public schema before restore.
|
||||
-h, --help Show this help.
|
||||
|
||||
Environment:
|
||||
USE_DEV_COMPOSE Set to 1 to include docker-compose.dev.yml
|
||||
USAGE
|
||||
}
|
||||
|
||||
dump_file=""
|
||||
wipe_public="0"
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--file)
|
||||
dump_file="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--wipe-public)
|
||||
wipe_public="1"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${dump_file}" ]]; then
|
||||
echo "--file is required" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -f "${dump_file}" ]]; then
|
||||
echo "Dump file not found: ${dump_file}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
compose_cmd=(docker compose -f "${project_root}/docker-compose.yml")
|
||||
if [[ "${USE_DEV_COMPOSE:-0}" == "1" && -f "${project_root}/docker-compose.dev.yml" ]]; then
|
||||
compose_cmd+=(-f "${project_root}/docker-compose.dev.yml")
|
||||
fi
|
||||
|
||||
if [[ "${wipe_public}" == "1" ]]; then
|
||||
"${compose_cmd[@]}" exec -T db sh -lc 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" "$POSTGRES_DB" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"'
|
||||
fi
|
||||
|
||||
case "${dump_file}" in
|
||||
*.dump)
|
||||
cat "${dump_file}" | "${compose_cmd[@]}" exec -T db sh -lc 'pg_restore --no-owner --no-acl -U "$POSTGRES_USER" -d "$POSTGRES_DB" -'
|
||||
;;
|
||||
*.sql)
|
||||
cat "${dump_file}" | "${compose_cmd[@]}" exec -T db sh -lc 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" "$POSTGRES_DB"'
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported file extension. Expected .dump or .sql" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Restore completed: ${dump_file}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue