1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 07:39:15 +03:00

BE sync / gitea

This commit is contained in:
Anton Afanasyeu
2026-06-10 20:22:08 +02:00
parent 10fade4a4d
commit 5e4f208d83
14 changed files with 781 additions and 17 deletions

View File

@@ -151,7 +151,7 @@
right: 0; right: 0;
bottom: var(--landing-footer-h); bottom: var(--landing-footer-h);
display: flex; display: flex;
z-index: 2; z-index: 5;
pointer-events: none; pointer-events: none;
} }
@@ -410,12 +410,20 @@
} }
/* (5) Footer stays platform-footer; lift above globe */ /* (5) Footer stays platform-footer; lift above globe */
.hub-landing .platform-footer { .hub-landing .bottom-bar.platform-footer {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
z-index: 12; 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 */ /* Clock widget below header */

View File

@@ -29,14 +29,15 @@
/* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d=1 */ /* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d=1 */
.hub-globe-canvas { .hub-globe-canvas {
position: fixed; position: fixed;
z-index: 3; z-index: 1;
pointer-events: auto; pointer-events: none;
cursor: grab; cursor: grab;
touch-action: none; touch-action: none;
border-radius: 50%; border-radius: 50%;
} }
.hub-globe-canvas.is-dragging { .hub-globe-canvas.is-dragging {
pointer-events: auto;
cursor: grabbing; cursor: grabbing;
} }

View File

@@ -904,11 +904,19 @@
inset: 0; inset: 0;
z-index: 20; z-index: 20;
} }
.hub-docs-modal[hidden] {
display: none !important;
pointer-events: none !important;
}
.hub-docs-backdrop { .hub-docs-backdrop {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: rgba(4, 8, 14, 0.66); background: rgba(4, 8, 14, 0.66);
pointer-events: auto;
}
.hub-docs-modal[hidden] .hub-docs-backdrop {
pointer-events: none;
} }
.hub-docs-panel { .hub-docs-panel {

View File

@@ -36,9 +36,9 @@ function hub_h(string $s): string {
})(); })();
</script> </script>
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css"> <link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
<link rel="stylesheet" href="hub.css?v=20260606landing"> <link rel="stylesheet" href="hub.css?v=20260610overlay">
<link rel="stylesheet" href="hub-logo-layers.css?v=20260606landing"> <link rel="stylesheet" href="hub-logo-layers.css?v=20260610overlay">
<link rel="stylesheet" href="hub-landing.css?v=20260606landing"> <link rel="stylesheet" href="hub-landing.css?v=20260610overlay">
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
@@ -415,10 +415,13 @@ function hub_h(string $s): string {
var docsPanelsById = {}; var docsPanelsById = {};
function closeDocs() { function closeDocs() {
if (!docsModal) return;
docsModal.hidden = true; docsModal.hidden = true;
docsModal.setAttribute('aria-hidden', 'true'); docsModal.setAttribute('aria-hidden', 'true');
} }
closeDocs();
function openDocs() { function openDocs() {
docsModal.hidden = false; docsModal.hidden = false;
docsModal.setAttribute('aria-hidden', 'false'); docsModal.setAttribute('aria-hidden', 'false');

View File

@@ -3,14 +3,20 @@ $bp = Auth::basePath();
$ab = assets_base(); $ab = assets_base();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login — <?= h(cfg('app_name')) ?></title> <title>Login — <?= h(cfg('app_name')) ?></title>
<script>
(function () {
var t = localStorage.getItem('crash_console_theme');
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
})();
</script>
<link rel="stylesheet" href="<?= h($ab) ?>/assets/css/app.css"> <link rel="stylesheet" href="<?= h($ab) ?>/assets/css/app.css">
</head> </head>
<body class="login-page"> <body class="login-page" data-base-path="<?= h($ab) ?>">
<form class="login-card" method="post" action="<?= h($bp) ?>/login"> <form class="login-card" method="post" action="<?= h($bp) ?>/login">
<h1><?= h(cfg('app_name')) ?></h1> <h1><?= h(cfg('app_name')) ?></h1>
<p class="muted">Sign in (same credentials as Crashes / Tickets)</p> <p class="muted">Sign in (same credentials as Crashes / Tickets)</p>

View File

@@ -27,6 +27,20 @@
--row-open: rgba(37, 99, 235, .06); --row-open: rgba(37, 99, 235, .06);
} }
* { box-sizing: border-box; } * { 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 { body {
margin: 0; margin: 0;
font-family: "Segoe UI", system-ui, sans-serif; font-family: "Segoe UI", system-ui, sans-serif;
@@ -470,7 +484,6 @@ body { --footer-h: 40px; }
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 2;
flex-shrink: 0; flex-shrink: 0;
height: var(--footer-h); height: var(--footer-h);
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
@@ -482,13 +495,26 @@ body { --footer-h: 40px; }
color: var(--muted); color: var(--muted);
background: var(--surface); background: var(--surface);
z-index: 40; 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 { .platform-footer {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
justify-content: center; justify-content: center;
height: auto; min-height: var(--footer-h);
min-height: 40px;
padding: 6px 16px; padding: 6px 16px;
gap: 4px; gap: 4px;
} }
@@ -526,10 +552,21 @@ body { --footer-h: 40px; }
min-height: 100dvh; min-height: 100dvh;
padding: 3rem 16px calc(var(--footer-h, 40px) + 1.5rem); padding: 3rem 16px calc(var(--footer-h, 40px) + 1.5rem);
box-sizing: border-box; box-sizing: border-box;
position: relative;
isolation: isolate;
z-index: 0;
} }
.login-page .login-card { .login-page .login-card {
position: relative; 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 { .login-card {
background: var(--surface); background: var(--surface);
@@ -768,6 +805,12 @@ body { --footer-h: 40px; }
z-index: 5; z-index: 5;
} }
.login-page .login-card { margin-top: 2.5rem; } .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 { .toolbar-select select {
padding: 6px 8px; padding: 6px 8px;
border-radius: 6px; border-radius: 6px;
@@ -1546,11 +1589,18 @@ button.report-tag--filter:hover {
justify-content: center; justify-content: center;
padding: 16px; padding: 16px;
} }
.tag-modal[hidden] { display: none; } .tag-modal[hidden] {
display: none !important;
pointer-events: none !important;
}
.tag-modal-backdrop { .tag-modal-backdrop {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: rgba(0, 0, 0, .55); background: rgba(0, 0, 0, .55);
pointer-events: auto;
}
.tag-modal[hidden] .tag-modal-backdrop {
pointer-events: none;
} }
.tag-modal-panel { .tag-modal-panel {
position: relative; position: relative;
@@ -1913,9 +1963,11 @@ button.report-tag--filter:hover {
background: rgba(8, 12, 20, 0.92); background: rgba(8, 12, 20, 0.92);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
pointer-events: auto;
} }
.graph-detail-overlay[hidden] { .graph-detail-overlay[hidden] {
display: none !important; display: none !important;
pointer-events: none !important;
} }
.graph-detail-panel { .graph-detail-panel {
display: flex; display: flex;

View File

@@ -25,6 +25,18 @@
return body ? body.getAttribute('data-base-path') || '' : ''; 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() { function storedLang() {
try { try {
const v = localStorage.getItem(STORAGE_KEY); const v = localStorage.getItem(STORAGE_KEY);
@@ -162,6 +174,7 @@
} }
async function init() { async function init() {
dismissPlatformOverlays();
lang = storedLang(); lang = storedLang();
document.documentElement.setAttribute('lang', lang); document.documentElement.setAttribute('lang', lang);
await setLang(lang, { silent: true }); await setLang(lang, { silent: true });

View File

@@ -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 Giteas 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

View File

@@ -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 <<EOF
{
"clone_addr": "$GITEA_MIRROR_URL",
"repo_name": "$GITEA_REPO",
"uid": $_uid,
"mirror": true,
"mirror_interval": "$GITEA_MIRROR_INTERVAL",
"private": $GITEA_PRIVATE,
"wiki": true,
"issues": false,
"milestones": false,
"labels": false,
"releases": true,
"description": "Pull mirror of $GITEA_MIRROR_URL"
}
EOF
)
log "API migrate (pull mirror) for uid=$_uid ..."
_http="$(curl -sS -o /tmp/gitea-migrate.json -w '%{http_code}' \
-X POST "$_api/repos/migrate" \
-H "Authorization: token $GITEA_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "$_payload")"
if [ "$_http" = "201" ] || [ "$_http" = "200" ]; then
log "mirror created via API (HTTP $_http)"
cat /tmp/gitea-migrate.json
printf '\n'
return 0
fi
# Repo may already exist — try sync mirror endpoint
if [ "$_http" = "409" ] || [ "$_http" = "422" ]; then
warn "migrate rejected (HTTP $_http) — repo may exist; try manual mirror settings or delete repo first"
fi
warn "API migrate failed HTTP $_http:"
cat /tmp/gitea-migrate.json >&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 <<EOF
Ongoing sync (no shared mount):
• Pull mirror (recommended): Gitea → Repo → Settings → Mirror → Pull mirror
Remote: $GITEA_MIRROR_URL
Interval: $GITEA_MIRROR_INTERVAL
• Or cron on BE (as git user):
*/15 * * * * gitea -c $GITEA_APP_INI admin repo-sync-mirrors
Clone URL for developers:
${GITEA_ROOT_URL}${GITEA_OWNER}/${GITEA_REPO}.git
EOF
}
# --- main ---
gitea_load_config || exit 1
log "Gitea ROOT_URL=$GITEA_ROOT_URL"
log "target repo: $GITEA_OWNER/$GITEA_REPO"
log "mirror from: $GITEA_MIRROR_URL"
if ! test_remote_reachable "$GITEA_MIRROR_URL"; then
warn "remote not reachable from this host"
warn "ensure git-daemon client access to f0xx.org:9418 or set GITEA_MIRROR_URL to https/ssh URL"
fi
if command -v rc-service >/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)"

View File

@@ -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"

View File

@@ -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
}

View File

@@ -13,13 +13,15 @@
$bp = Auth::basePath(); $bp = Auth::basePath();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login — <?= h(cfg('app_name')) ?></title> <title>Login — <?= h(cfg('app_name')) ?></title>
<script> <script>
(function () { (function () {
var t = localStorage.getItem('crash_console_theme');
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
var l = localStorage.getItem('crash_console_lang'); var l = localStorage.getItem('crash_console_lang');
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l); if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
})(); })();

View File

@@ -2,13 +2,15 @@
$bp = Auth::basePath(); $bp = Auth::basePath();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Register — <?= h(cfg('app_name')) ?></title> <title>Register — <?= h(cfg('app_name')) ?></title>
<script> <script>
(function () { (function () {
var t = localStorage.getItem('crash_console_theme');
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
var l = localStorage.getItem('crash_console_lang'); var l = localStorage.getItem('crash_console_lang');
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l); if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
})(); })();

View File

@@ -2,13 +2,15 @@
$bp = Auth::basePath(); $bp = Auth::basePath();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Two-factor — <?= h(cfg('app_name')) ?></title> <title>Two-factor — <?= h(cfg('app_name')) ?></title>
<script> <script>
(function () { (function () {
var t = localStorage.getItem('crash_console_theme');
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
var l = localStorage.getItem('crash_console_lang'); var l = localStorage.getItem('crash_console_lang');
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l); if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
})(); })();