From 5e4f208d83b88e4a625d4cf8a0285fa7bfb081f8 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Wed, 10 Jun 2026 20:22:08 +0200 Subject: [PATCH] BE sync / gitea --- examples/app_hub/hub-landing.css | 12 +- examples/app_hub/hub-logo-layers.css | 5 +- examples/app_hub/hub.css | 8 + examples/app_hub/index.php | 9 +- .../build_console/backend/views/login.php | 10 +- .../backend/public/assets/css/app.css | 62 ++++- .../backend/public/assets/js/i18n.js | 13 + .../backend/scripts/gitea/README.md | 92 +++++++ .../scripts/gitea/gitea_attach_remote.sh | 221 +++++++++++++++ .../scripts/gitea/gitea_collect_backup.sh | 251 ++++++++++++++++++ .../backend/scripts/gitea/gitea_ini.sh | 103 +++++++ .../crash_reporter/backend/views/login.php | 4 +- .../crash_reporter/backend/views/register.php | 4 +- .../backend/views/two_factor_challenge.php | 4 +- 14 files changed, 781 insertions(+), 17 deletions(-) create mode 100644 examples/crash_reporter/backend/scripts/gitea/README.md create mode 100755 examples/crash_reporter/backend/scripts/gitea/gitea_attach_remote.sh create mode 100755 examples/crash_reporter/backend/scripts/gitea/gitea_collect_backup.sh create mode 100755 examples/crash_reporter/backend/scripts/gitea/gitea_ini.sh diff --git a/examples/app_hub/hub-landing.css b/examples/app_hub/hub-landing.css index c3dfb1f..d4c264e 100644 --- a/examples/app_hub/hub-landing.css +++ b/examples/app_hub/hub-landing.css @@ -151,7 +151,7 @@ right: 0; bottom: var(--landing-footer-h); display: flex; - z-index: 2; + z-index: 5; pointer-events: none; } @@ -410,12 +410,20 @@ } /* (5) Footer stays platform-footer; lift above globe */ -.hub-landing .platform-footer { +.hub-landing .bottom-bar.platform-footer { position: fixed; bottom: 0; left: 0; right: 0; z-index: 12; + height: var(--landing-footer-h); + min-height: var(--landing-footer-h); + max-height: var(--landing-footer-h); + pointer-events: none; +} +.hub-landing .bottom-bar.platform-footer a, +.hub-landing .bottom-bar.platform-footer button { + pointer-events: auto; } /* Clock widget below header */ diff --git a/examples/app_hub/hub-logo-layers.css b/examples/app_hub/hub-logo-layers.css index 8139383..943ae3d 100644 --- a/examples/app_hub/hub-logo-layers.css +++ b/examples/app_hub/hub-logo-layers.css @@ -29,14 +29,15 @@ /* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d=1 */ .hub-globe-canvas { position: fixed; - z-index: 3; - pointer-events: auto; + z-index: 1; + pointer-events: none; cursor: grab; touch-action: none; border-radius: 50%; } .hub-globe-canvas.is-dragging { + pointer-events: auto; cursor: grabbing; } diff --git a/examples/app_hub/hub.css b/examples/app_hub/hub.css index 56322a1..eb43e1b 100644 --- a/examples/app_hub/hub.css +++ b/examples/app_hub/hub.css @@ -904,11 +904,19 @@ inset: 0; z-index: 20; } +.hub-docs-modal[hidden] { + display: none !important; + pointer-events: none !important; +} .hub-docs-backdrop { position: absolute; inset: 0; background: rgba(4, 8, 14, 0.66); + pointer-events: auto; +} +.hub-docs-modal[hidden] .hub-docs-backdrop { + pointer-events: none; } .hub-docs-panel { diff --git a/examples/app_hub/index.php b/examples/app_hub/index.php index bdda573..1c628f2 100644 --- a/examples/app_hub/index.php +++ b/examples/app_hub/index.php @@ -36,9 +36,9 @@ function hub_h(string $s): string { })(); - - - + + + - +

Sign in (same credentials as Crashes / Tickets)

diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index 7f769ef..3afbb14 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -27,6 +27,20 @@ --row-open: rgba(37, 99, 235, .06); } * { box-sizing: border-box; } +[hidden] { + display: none !important; + pointer-events: none !important; +} +dialog:not([open]) { + display: none !important; +} +dialog:not([open])::backdrop { + display: none !important; + pointer-events: none !important; +} +dialog::backdrop { + pointer-events: auto; +} body { margin: 0; font-family: "Segoe UI", system-ui, sans-serif; @@ -470,7 +484,6 @@ body { --footer-h: 40px; } left: 0; right: 0; bottom: 0; - z-index: 2; flex-shrink: 0; height: var(--footer-h); border-top: 1px solid var(--border); @@ -482,13 +495,26 @@ body { --footer-h: 40px; } color: var(--muted); background: var(--surface); z-index: 40; + pointer-events: none; +} +.bottom-bar a, +.bottom-bar button, +.bottom-bar input, +.bottom-bar select, +.bottom-bar textarea { + pointer-events: auto; +} +.bottom-bar.platform-footer { + height: var(--footer-h); + min-height: var(--footer-h); + max-height: var(--footer-h); + overflow: hidden; } .platform-footer { flex-direction: column; align-items: stretch; justify-content: center; - height: auto; - min-height: 40px; + min-height: var(--footer-h); padding: 6px 16px; gap: 4px; } @@ -526,10 +552,21 @@ body { --footer-h: 40px; } min-height: 100dvh; padding: 3rem 16px calc(var(--footer-h, 40px) + 1.5rem); box-sizing: border-box; + position: relative; + isolation: isolate; + z-index: 0; } .login-page .login-card { position: relative; - z-index: 6; + z-index: 100; + pointer-events: auto; +} +.login-page .login-locale { + z-index: 101; + pointer-events: auto; +} +.login-page .bottom-bar { + z-index: 50; } .login-card { background: var(--surface); @@ -768,6 +805,12 @@ body { --footer-h: 40px; } z-index: 5; } .login-page .login-card { margin-top: 2.5rem; } +.login-card input, +.login-card button, +.login-card textarea, +.login-card select { + pointer-events: auto; +} .toolbar-select select { padding: 6px 8px; border-radius: 6px; @@ -1546,11 +1589,18 @@ button.report-tag--filter:hover { justify-content: center; padding: 16px; } -.tag-modal[hidden] { display: none; } +.tag-modal[hidden] { + display: none !important; + pointer-events: none !important; +} .tag-modal-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, .55); + pointer-events: auto; +} +.tag-modal[hidden] .tag-modal-backdrop { + pointer-events: none; } .tag-modal-panel { position: relative; @@ -1913,9 +1963,11 @@ button.report-tag--filter:hover { background: rgba(8, 12, 20, 0.92); display: flex; flex-direction: column; + pointer-events: auto; } .graph-detail-overlay[hidden] { display: none !important; + pointer-events: none !important; } .graph-detail-panel { display: flex; diff --git a/examples/crash_reporter/backend/public/assets/js/i18n.js b/examples/crash_reporter/backend/public/assets/js/i18n.js index 6d17365..c858bb0 100644 --- a/examples/crash_reporter/backend/public/assets/js/i18n.js +++ b/examples/crash_reporter/backend/public/assets/js/i18n.js @@ -25,6 +25,18 @@ return body ? body.getAttribute('data-base-path') || '' : ''; } + /** Close stray modal layers so fixed backdrops never block login/console UI. */ + function dismissPlatformOverlays() { + document.querySelectorAll('#graph-detail-overlay, #tag-modal, #hub-docs-modal').forEach((el) => { + el.hidden = true; + el.setAttribute('aria-hidden', 'true'); + }); + const dialog = document.getElementById('ticket-create-dialog'); + if (dialog && dialog.open && typeof dialog.close === 'function') { + dialog.close(); + } + } + function storedLang() { try { const v = localStorage.getItem(STORAGE_KEY); @@ -162,6 +174,7 @@ } async function init() { + dismissPlatformOverlays(); lang = storedLang(); document.documentElement.setAttribute('lang', lang); await setLang(lang, { silent: true }); diff --git a/examples/crash_reporter/backend/scripts/gitea/README.md b/examples/crash_reporter/backend/scripts/gitea/README.md new file mode 100644 index 0000000..d43d1cd --- /dev/null +++ b/examples/crash_reporter/backend/scripts/gitea/README.md @@ -0,0 +1,92 @@ +# Gitea backup and remote attach (Alpine BE) + +Scripts for the BE Gitea instance at `https://apps.f0xx.org/app/androidcast_project/git/`. + +**Safety:** The main working tree under +`/var/www/localhost/htdocs/apps/app/androidcast_project/android_cast` +is **never** modified. Only Gitea-owned paths from `/etc/gitea/app.ini` are backed up or reset. + +## Prerequisites (Alpine BE) + +- Gitea installed (`apk add gitea`), config at `/etc/gitea/app.ini` +- Run backup as **root** (service control + mysqldump) +- For attach: `git`, `curl`; **`GITEA_ADMIN_TOKEN`** (admin PAT from Gitea UI) + +## 1. Full backup + optional reset + +```bash +cd /path/to/android_cast/examples/crash_reporter/backend/scripts/gitea +chmod +x gitea_collect_backup.sh gitea_attach_remote.sh gitea_ini.sh + +# default output: /var/backups/gitea/gitea-collect-YYYYMMDD-HHMMSS/ +./gitea_collect_backup.sh +``` + +Collects: + +| Item | Source (from `app.ini`) | +|------|-------------------------| +| Config | `/etc/gitea/app.ini` | +| App data | `[server] APP_DATA_PATH` (caches, indexes, attachments) | +| Bare repos | `[repository] ROOT` | +| LFS | `[lfs] PATH` (if set) | +| Logs | `[log] ROOT_PATH` | +| DB | `mysqldump` / `pg_dump` / sqlite file copy | +| Built-in dump | `gitea dump` when available | + +At the end: + +```text +Would you like to reset Gitea repositories state to the default (empty)? [y/N] +``` + +- **`N`** (default): backup only +- **`y`**: stops Gitea, `gitea admin repo-delete` per repo, clears **only** `[repository] ROOT` (must be under `/var/lib/gitea/`), restarts Gitea + +## 2. Attach canonical remote as pull mirror + +After backup (and optional reset), point Gitea at the real remote: + +```bash +export GITEA_ADMIN_TOKEN='your-admin-pat' + +# defaults: git://f0xx.org/android_cast → foxx/android_cast +./gitea_attach_remote.sh +``` + +Override: + +```bash +GITEA_MIRROR_URL='git://f0xx.org/android_cast' \ +GITEA_OWNER='foxx' \ +GITEA_REPO='android_cast' \ +GITEA_MIRROR_INTERVAL='10m' \ +./gitea_attach_remote.sh +``` + +**Method (no NFS/SSHFS):** + +1. **Preferred:** Gitea API `POST /api/v1/repos/migrate` with `"mirror": true` — UI updates on Gitea’s mirror interval (`10m` default). +2. **Fallback:** `git clone --mirror` from remote + `git push --mirror` to local Gitea HTTP. + +Ongoing sync: Gitea pull mirror or cron `gitea admin repo-sync-mirrors`. + +## Verify + +```bash +# Remote reachable from BE +git ls-remote git://f0xx.org/android_cast HEAD + +# Gitea UI +curl -sS -o /dev/null -w '%{http_code}\n' \ + 'https://apps.f0xx.org/app/androidcast_project/git/' + +# After attach +git ls-remote "https://apps.f0xx.org/app/androidcast_project/git/foxx/android_cast.git" HEAD +``` + +## Related docs + +- [docs/INFRA.md](../../../../docs/INFRA.md) — `git://f0xx.org/android_cast`, disk layout +- [docs/TICKETS_ROADMAP.md](../../../../docs/TICKETS_ROADMAP.md) — Gitea URL / `ROOT_URL` +- [docs/GIT_FLOW.md](../../../../docs/GIT_FLOW.md) — branch flow after remote is wired diff --git a/examples/crash_reporter/backend/scripts/gitea/gitea_attach_remote.sh b/examples/crash_reporter/backend/scripts/gitea/gitea_attach_remote.sh new file mode 100755 index 0000000..c75e67f --- /dev/null +++ b/examples/crash_reporter/backend/scripts/gitea/gitea_attach_remote.sh @@ -0,0 +1,221 @@ +#!/bin/ash +# +# Attach canonical git:// remote to Gitea as a pull mirror (no NFS/SSHFS shared folder). +# Run on Alpine BE after gitea_collect_backup.sh (optional reset) and Gitea is running. +# +# Default remote matches docs/INFRA.md: git://f0xx.org/android_cast +# +# Usage: +# export GITEA_ADMIN_TOKEN='...' # admin personal access token (recommended) +# ./gitea_attach_remote.sh +# +# GITEA_MIRROR_URL=git://f0xx.org/android_cast \ +# GITEA_OWNER=foxx GITEA_REPO=android_cast \ +# ./gitea_attach_remote.sh +# +# Methods (auto): +# 1) Gitea API /repos/migrate with mirror=true (fastest ongoing sync in UI) +# 2) git clone --mirror + git push --mirror to local Gitea HTTP remote +# +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=gitea_ini.sh +. "$SCRIPT_DIR/gitea_ini.sh" + +GITEA_MIRROR_URL="${GITEA_MIRROR_URL:-git://f0xx.org/android_cast}" +GITEA_OWNER="${GITEA_OWNER:-foxx}" +GITEA_REPO="${GITEA_REPO:-android_cast}" +GITEA_MIRROR_INTERVAL="${GITEA_MIRROR_INTERVAL:-10m}" +GITEA_PRIVATE="${GITEA_PRIVATE:-false}" +WORK_DIR="${WORK_DIR:-/tmp/gitea-attach-$$}" + +log() { printf '[gitea-attach] %s\n' "$*"; } +warn() { printf '[gitea-attach] WARN: %s\n' "$*" >&2; } +die() { printf '[gitea-attach] ERROR: %s\n' "$*" >&2; exit 1; } + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "missing command: $1" +} + +gitea_api_base() { + _url="$GITEA_ROOT_URL" + case "$_url" in + */) _url="${_url%/}" ;; + esac + printf '%s/api/v1' "$_url" +} + +gitea_local_http_base() { + printf 'http://127.0.0.1:%s' "${GITEA_HTTP_PORT:-3000}" +} + +# ROOT_URL may include subpath; derive path prefix for local git push URL. +gitea_subpath_prefix() { + _root="$GITEA_ROOT_URL" + _root="${_root#http://}" + _root="${_root#https://}" + _root="${_root#*@}" # drop user@ if any + _root="${_root#*/}" # drop host + case "$_root" in + */*) printf '/%s' "${_root%/}" ;; + *) printf '' ;; + esac +} + +test_remote_reachable() { + _url="$1" + log "probing remote: $_url" + if command -v git >/dev/null 2>&1; then + if git ls-remote "$_url" HEAD >/dev/null 2>&1; then + log "remote reachable (git ls-remote HEAD)" + return 0 + fi + warn "git ls-remote failed for $_url" + fi + return 1 +} + +gitea_owner_uid() { + need_cmd curl + [ -n "${GITEA_ADMIN_TOKEN:-}" ] || return 1 + _api="$(gitea_api_base)" + _uid="$(curl -sS "$_api/users/$GITEA_OWNER" \ + -H "Authorization: token $GITEA_ADMIN_TOKEN" \ + | awk -F'"' '/"id"/ { print $4; exit }')" + [ -n "$_uid" ] || return 1 + printf '%s' "$_uid" +} + +migrate_via_api() { + need_cmd curl + [ -n "${GITEA_ADMIN_TOKEN:-}" ] || return 1 + + _uid="$(gitea_owner_uid)" || die "could not resolve uid for owner $GITEA_OWNER (check token and user exists)" + + _api="$(gitea_api_base)" + _payload=$(cat <&2 + return 1 +} + +mirror_via_git() { + need_cmd git + mkdir -p "$WORK_DIR" + _mirror="$WORK_DIR/${GITEA_REPO}.mirror.git" + + log "git clone --mirror $GITEA_MIRROR_URL" + rm -rf "$_mirror" + git clone --mirror "$GITEA_MIRROR_URL" "$_mirror" + + _prefix="$(gitea_subpath_prefix)" + _push_url="${GITEA_PUSH_URL:-$(gitea_local_http_base)${_prefix}/${GITEA_OWNER}/${GITEA_REPO}.git}" + + log "creating empty repo in Gitea (if needed) via API or assume exists" + if [ -n "${GITEA_ADMIN_TOKEN:-}" ]; then + _api="$(gitea_api_base)" + curl -sS -o /dev/null -w '' \ + -X POST "$_api/admin/users/$GITEA_OWNER/repos" \ + -H "Authorization: token $GITEA_ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"$GITEA_REPO\",\"private\":$GITEA_PRIVATE}" \ + 2>/dev/null || true + fi + + log "git push --mirror -> $_push_url" + if [ -n "${GITEA_ADMIN_TOKEN:-}" ]; then + _auth_url="$(gitea_local_http_base)${_prefix}/${GITEA_OWNER}/${GITEA_REPO}.git" + _auth_url="${_auth_url#http://}" + git -C "$_mirror" push --mirror "http://${GITEA_ADMIN_TOKEN}@${_auth_url}" + else + warn "GITEA_ADMIN_TOKEN unset — push may fail; set token or configure git credentials" + git -C "$_mirror" push --mirror "$_push_url" + fi + + log "one-time mirror push complete" + warn "for ongoing sync, enable pull mirror in Gitea UI or re-run API migrate method" + rm -rf "$WORK_DIR" +} + +enable_mirror_sync_cron_hint() { + cat </dev/null 2>&1; then + if ! rc-service gitea status 2>/dev/null | grep -qi started; then + warn "gitea service may not be running — starting..." + rc-service gitea start 2>/dev/null || true + fi +fi + +if migrate_via_api; then + enable_mirror_sync_cron_hint + log "done (API pull mirror)" + exit 0 +fi + +log "falling back to git clone --mirror + push --mirror" +mirror_via_git +enable_mirror_sync_cron_hint +log "done (git mirror push — configure pull mirror in UI for automatic updates)" diff --git a/examples/crash_reporter/backend/scripts/gitea/gitea_collect_backup.sh b/examples/crash_reporter/backend/scripts/gitea/gitea_collect_backup.sh new file mode 100755 index 0000000..9236615 --- /dev/null +++ b/examples/crash_reporter/backend/scripts/gitea/gitea_collect_backup.sh @@ -0,0 +1,251 @@ +#!/bin/ash +# +# Collect full Gitea backup on Alpine BE (parse /etc/gitea/app.ini). +# Run as root on the BE where Gitea is installed. +# +# Usage: +# ./gitea_collect_backup.sh +# OUT_BASE=/root/gitea-backups ./gitea_collect_backup.sh +# GITEA_INI=/etc/gitea/app.ini ./gitea_collect_backup.sh +# +# Does NOT touch the main working tree: +# /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast +# +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=gitea_ini.sh +. "$SCRIPT_DIR/gitea_ini.sh" + +OUT_BASE="${OUT_BASE:-/var/backups/gitea}" +STAMP="$(date +%Y%m%d-%H%M%S)" +OUT_DIR="$OUT_BASE/gitea-collect-$STAMP" +MANIFEST="$OUT_DIR/MANIFEST.txt" + +# Paths that must never be deleted or overwritten by the optional reset step. +PROTECTED_MAIN_REPO="${PROTECTED_MAIN_REPO:-/var/www/localhost/htdocs/apps/app/androidcast_project/android_cast}" + +log() { printf '[gitea-collect] %s\n' "$*"; } +warn() { printf '[gitea-collect] WARN: %s\n' "$*" >&2; } +die() { printf '[gitea-collect] ERROR: %s\n' "$*" >&2; exit 1; } + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "missing command: $1" +} + +copy_tree() { + _src="$1" + _dst="$2" + if [ ! -e "$_src" ]; then + warn "skip missing: $_src" + return 0 + fi + mkdir -p "$(dirname "$_dst")" + if cp -a "$_src" "$_dst" 2>/dev/null; then + log "copied: $_src -> $_dst" + return 0 + fi + # BusyBox cp may lack -a; try tar pipe. + mkdir -p "$_dst" + (cd "$_src" && tar cf - .) | (cd "$_dst" && tar xf -) + log "archived (tar): $_src -> $_dst" +} + +dump_mysql() { + _out="$1" + need_cmd mysqldump + _args="" + if [ -n "$GITEA_DB_HOST" ]; then + case "$GITEA_DB_HOST" in + /*) _args="--socket=$GITEA_DB_HOST" ;; + *) _args="-h $GITEA_DB_HOST" ;; + esac + fi + # shellcheck disable=SC2086 + mysqldump $_args -u"$GITEA_DB_USER" ${GITEA_DB_PASS:+-p"$GITEA_DB_PASS"} \ + --single-transaction --routines --triggers "$GITEA_DB_NAME" > "$_out" + log "mysqldump -> $_out" +} + +dump_postgres() { + _out="$1" + need_cmd pg_dump + PGPASSWORD="$GITEA_DB_PASS" pg_dump -h "${GITEA_DB_HOST:-127.0.0.1}" -U "$GITEA_DB_USER" "$GITEA_DB_NAME" > "$_out" + log "pg_dump -> $_out" +} + +write_manifest() { + { + echo "Gitea collect backup" + echo "created_at=$STAMP" + echo "hostname=$(hostname 2>/dev/null || echo unknown)" + echo "gitea_ini=$GITEA_APP_INI" + echo "repo_root=$GITEA_REPO_ROOT" + echo "app_data=$GITEA_APP_DATA" + echo "root_url=$GITEA_ROOT_URL" + echo "db_type=$GITEA_DB_TYPE" + echo "db_name=$GITEA_DB_NAME" + echo "lfs_path=$GITEA_LFS_PATH" + echo "log_root=$GITEA_LOG_ROOT" + echo "protected_main_repo=$PROTECTED_MAIN_REPO" + echo "" + echo "--- gitea version ---" + gitea_cli "-v" 2>/dev/null || echo "gitea -v unavailable" + echo "" + echo "--- repo directories ---" + if [ -d "$GITEA_REPO_ROOT" ]; then + find "$GITEA_REPO_ROOT" -maxdepth 3 -type d -name '*.git' 2>/dev/null || true + else + echo "(repo root missing)" + fi + } > "$MANIFEST" +} + +list_gitea_repos() { + if [ ! -d "$GITEA_REPO_ROOT" ]; then + return 0 + fi + find "$GITEA_REPO_ROOT" -mindepth 2 -maxdepth 2 -type d -name '*.git' 2>/dev/null \ + | while read -r gitdir; do + owner="$(basename "$(dirname "$gitdir")")" + repo="$(basename "$gitdir" .git)" + printf '%s/%s\n' "$owner" "$repo" + done +} + +delete_gitea_repo_records() { + _owner="$1" + _repo="$2" + if gitea_cli "admin" "repo" "delete" "$_owner/$_repo" 2>/dev/null; then + log "gitea admin repo delete $_owner/$_repo" + return 0 + fi + if gitea_cli "admin" "repo-delete" "$_owner/$_repo" 2>/dev/null; then + log "gitea admin repo-delete $_owner/$_repo" + return 0 + fi + warn "could not delete $_owner/$_repo via gitea CLI (will still remove bare files)" + return 1 +} + +reset_gitea_repos_only() { + if ! gitea_path_is_safe_to_wipe "$GITEA_REPO_ROOT"; then + die "refusing reset: repo root not under safe prefix: $GITEA_REPO_ROOT" + fi + case "$PROTECTED_MAIN_REPO" in + "$GITEA_REPO_ROOT"|"$GITEA_REPO_ROOT"/*) + die "refusing reset: Gitea repo root overlaps protected main repo path" + ;; + esac + + log "stopping gitea (if running)..." + if command -v rc-service >/dev/null 2>&1; then + rc-service gitea stop 2>/dev/null || true + fi + + log "removing Gitea repo registrations via CLI..." + _repos="$(list_gitea_repos)" + if [ -n "$_repos" ]; then + echo "$_repos" | while read -r slug; do + [ -n "$slug" ] || continue + _o="${slug%%/*}" + _r="${slug#*/}" + delete_gitea_repo_records "$_o" "$_r" || true + done + fi + + if [ -d "$GITEA_REPO_ROOT" ]; then + log "clearing bare repos under $GITEA_REPO_ROOT (not touching $PROTECTED_MAIN_REPO)" + find "$GITEA_REPO_ROOT" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + + fi + mkdir -p "$GITEA_REPO_ROOT" + + log "starting gitea..." + if command -v rc-service >/dev/null 2>&1; then + rc-service gitea start 2>/dev/null || warn "rc-service gitea start failed — start manually" + fi + log "Gitea repository store reset to empty (Gitea DB may still need doctor if UI shows ghosts)" +} + +# --- main --- + +[ "$(id -u)" -eq 0 ] || die "run as root (needed for service stop, file reads, mysqldump)" + +gitea_load_config || exit 1 +need_cmd awk +need_cmd mkdir +need_cmd date + +mkdir -p "$OUT_DIR" +log "backup directory: $OUT_DIR" + +write_manifest + +log "copy app.ini" +mkdir -p "$OUT_DIR/config" +copy_tree "$GITEA_APP_INI" "$OUT_DIR/config/app.ini" + +log "copy APP_DATA_PATH (includes caches, indexes, attachments, custom/)" +copy_tree "$GITEA_APP_DATA" "$OUT_DIR/app_data" + +log "copy repository ROOT (bare repos)" +copy_tree "$GITEA_REPO_ROOT" "$OUT_DIR/gitea-repositories" + +if [ -n "$GITEA_LFS_PATH" ] && [ "$GITEA_LFS_PATH" != "$GITEA_APP_DATA/lfs" ]; then + log "copy LFS path" + copy_tree "$GITEA_LFS_PATH" "$OUT_DIR/lfs" +fi + +if [ -n "$GITEA_LOG_ROOT" ] && [ -d "$GITEA_LOG_ROOT" ]; then + log "copy log directory" + copy_tree "$GITEA_LOG_ROOT" "$OUT_DIR/logs" +fi + +log "database dump" +mkdir -p "$OUT_DIR/database" +case "$(echo "$GITEA_DB_TYPE" | tr '[:upper:]' '[:lower:]')" in + mysql) + dump_mysql "$OUT_DIR/database/gitea-mysql.sql" + ;; + postgres|postgresql) + dump_postgres "$OUT_DIR/database/gitea-postgres.sql" + ;; + sqlite3|sqlite) + if [ -n "$GITEA_DB_PATH" ] && [ -f "$GITEA_DB_PATH" ]; then + copy_tree "$GITEA_DB_PATH" "$OUT_DIR/database/gitea.sqlite3" + elif [ -f "$GITEA_APP_DATA/gitea.db" ]; then + copy_tree "$GITEA_APP_DATA/gitea.db" "$OUT_DIR/database/gitea.sqlite3" + else + warn "sqlite DB file not found (database PATH / gitea.db)" + fi + ;; + *) + warn "unknown DB_TYPE=$GITEA_DB_TYPE — copy app_data only" + ;; +esac + +if command -v gitea >/dev/null 2>&1; then + if gitea_cli "dump" "--file" "$OUT_DIR/gitea-built-in-dump.zip" 2>/dev/null; then + log "gitea built-in dump -> gitea-built-in-dump.zip" + else + warn "gitea dump subcommand unavailable or failed (backup copies above are still valid)" + fi +fi + +_bytes="$(du -sh "$OUT_DIR" 2>/dev/null | awk '{print $1}')" +log "done. size≈${_bytes:-?} manifest=$MANIFEST" +log "protected working tree (never touched): $PROTECTED_MAIN_REPO" + +printf '\nWould you like to reset Gitea repositories state to the default (empty)? [y/N] ' +read -r _reset_answer || _reset_answer="" +case "$(echo "$_reset_answer" | tr '[:upper:]' '[:lower:]')" in + y|yes) + log "reset confirmed" + reset_gitea_repos_only + ;; + *) + log "reset skipped" + ;; +esac + +log "backup complete: $OUT_DIR" diff --git a/examples/crash_reporter/backend/scripts/gitea/gitea_ini.sh b/examples/crash_reporter/backend/scripts/gitea/gitea_ini.sh new file mode 100755 index 0000000..6c784b4 --- /dev/null +++ b/examples/crash_reporter/backend/scripts/gitea/gitea_ini.sh @@ -0,0 +1,103 @@ +#!/bin/ash +# Shared /etc/gitea/app.ini helpers for ash/bash (Alpine BE). +# shellcheck shell=sh + +gitea_ini_file="${GITEA_INI:-/etc/gitea/app.ini}" + +# ini_get FILE SECTION KEY -> prints value (last PATH wins without section; use section) +ini_get() { + _ini_file="$1" + _ini_section="$2" + _ini_key="$3" + awk -F= -v sec="[$_ini_section]" -v key="$_ini_key" ' + function trim(s) { + sub(/^[ \t\r\n]+/, "", s) + sub(/[ \t\r\n]+$/, "", s) + return s + } + function unquote(s) { + s = trim(s) + if (s ~ /^".*"$/) { sub(/^"/, "", s); sub(/"$/, "", s) } + if (s ~ /^'\''.*'\''$/) { sub(/^'\''/, "", s); sub(/'\''$/, "", s) } + return s + } + /^[ \t]*#/ { next } + /^[ \t]*;/ { next } + /^\[/ { + cur = $0 + gsub(/^\[|\]$/, "", cur) + cur = trim(cur) + next + } + { + k = trim($1) + if (k != key) next + if (sec != "" && cur != sec) next + v = $0 + sub(/^[^=]*=/, "", v) + print unquote(v) + exit + } + ' "$_ini_file" +} + +gitea_load_config() { + if [ ! -r "$gitea_ini_file" ]; then + echo "ERROR: cannot read $gitea_ini_file" >&2 + return 1 + fi + + GITEA_APP_INI="$gitea_ini_file" + GITEA_REPO_ROOT="$(ini_get "$gitea_ini_file" repository ROOT)" + GITEA_APP_DATA="$(ini_get "$gitea_ini_file" server APP_DATA_PATH)" + GITEA_ROOT_URL="$(ini_get "$gitea_ini_file" server ROOT_URL)" + GITEA_HTTP_PORT="$(ini_get "$gitea_ini_file" server HTTP_PORT)" + GITEA_DOMAIN="$(ini_get "$gitea_ini_file" server DOMAIN)" + GITEA_DB_TYPE="$(ini_get "$gitea_ini_file" database DB_TYPE)" + GITEA_DB_HOST="$(ini_get "$gitea_ini_file" database HOST)" + GITEA_DB_NAME="$(ini_get "$gitea_ini_file" database NAME)" + GITEA_DB_USER="$(ini_get "$gitea_ini_file" database USER)" + GITEA_DB_PASS="$(ini_get "$gitea_ini_file" database PASSWD)" + GITEA_DB_PATH="$(ini_get "$gitea_ini_file" database PATH)" + GITEA_LFS_PATH="$(ini_get "$gitea_ini_file" lfs PATH)" + GITEA_LOG_ROOT="$(ini_get "$gitea_ini_file" log ROOT_PATH)" + + [ -n "$GITEA_APP_DATA" ] || GITEA_APP_DATA="/var/lib/gitea" + [ -n "$GITEA_REPO_ROOT" ] || GITEA_REPO_ROOT="$GITEA_APP_DATA/data/gitea-repositories" + [ -n "$GITEA_HTTP_PORT" ] || GITEA_HTTP_PORT="3000" + [ -n "$GITEA_DB_TYPE" ] || GITEA_DB_TYPE="sqlite3" + + export GITEA_APP_INI GITEA_REPO_ROOT GITEA_APP_DATA GITEA_ROOT_URL GITEA_HTTP_PORT + export GITEA_DOMAIN GITEA_DB_TYPE GITEA_DB_HOST GITEA_DB_NAME GITEA_DB_USER GITEA_DB_PASS + export GITEA_DB_PATH GITEA_LFS_PATH GITEA_LOG_ROOT +} + +# Must be absolute, no .., and under allowed prefixes (Gitea-owned paths only). +gitea_path_is_safe_to_wipe() { + _target="$1" + case "$_target" in + ""|/*..*|*../*) return 1 ;; + esac + case "$_target" in + /var/lib/gitea/*|/var/lib/gitea) return 0 ;; + /data/gitea/*|/data/gitea) return 0 ;; + esac + return 1 +} + +gitea_run_as_git() { + if id git >/dev/null 2>&1; then + su -s /bin/sh git -c "$*" + else + sh -c "$*" + fi +} + +gitea_cli() { + if command -v gitea >/dev/null 2>&1; then + gitea_run_as_git "gitea -c '$GITEA_APP_INI' $*" + else + echo "WARN: gitea binary not in PATH" >&2 + return 1 + fi +} diff --git a/examples/crash_reporter/backend/views/login.php b/examples/crash_reporter/backend/views/login.php index 6818e79..9c07d58 100644 --- a/examples/crash_reporter/backend/views/login.php +++ b/examples/crash_reporter/backend/views/login.php @@ -13,13 +13,15 @@ $bp = Auth::basePath(); ?> - + Login — <?= h(cfg('app_name')) ?>