mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:58:14 +03:00
BE sync / gitea
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -36,9 +36,9 @@ function hub_h(string $s): string {
|
||||
})();
|
||||
</script>
|
||||
<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-logo-layers.css?v=20260606landing">
|
||||
<link rel="stylesheet" href="hub-landing.css?v=20260606landing">
|
||||
<link rel="stylesheet" href="hub.css?v=20260610overlay">
|
||||
<link rel="stylesheet" href="hub-logo-layers.css?v=20260610overlay">
|
||||
<link rel="stylesheet" href="hub-landing.css?v=20260610overlay">
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
@@ -415,10 +415,13 @@ function hub_h(string $s): string {
|
||||
var docsPanelsById = {};
|
||||
|
||||
function closeDocs() {
|
||||
if (!docsModal) return;
|
||||
docsModal.hidden = true;
|
||||
docsModal.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
closeDocs();
|
||||
|
||||
function openDocs() {
|
||||
docsModal.hidden = false;
|
||||
docsModal.setAttribute('aria-hidden', 'false');
|
||||
|
||||
@@ -3,14 +3,20 @@ $bp = Auth::basePath();
|
||||
$ab = assets_base();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<body class="login-page" data-base-path="<?= h($ab) ?>">
|
||||
<form class="login-card" method="post" action="<?= h($bp) ?>/login">
|
||||
<h1><?= h(cfg('app_name')) ?></h1>
|
||||
<p class="muted">Sign in (same credentials as Crashes / Tickets)</p>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
92
examples/crash_reporter/backend/scripts/gitea/README.md
Normal file
92
examples/crash_reporter/backend/scripts/gitea/README.md
Normal 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 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
|
||||
221
examples/crash_reporter/backend/scripts/gitea/gitea_attach_remote.sh
Executable file
221
examples/crash_reporter/backend/scripts/gitea/gitea_attach_remote.sh
Executable 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)"
|
||||
251
examples/crash_reporter/backend/scripts/gitea/gitea_collect_backup.sh
Executable file
251
examples/crash_reporter/backend/scripts/gitea/gitea_collect_backup.sh
Executable 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"
|
||||
103
examples/crash_reporter/backend/scripts/gitea/gitea_ini.sh
Executable file
103
examples/crash_reporter/backend/scripts/gitea/gitea_ini.sh
Executable 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
|
||||
}
|
||||
@@ -13,13 +13,15 @@
|
||||
$bp = Auth::basePath();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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);
|
||||
var l = localStorage.getItem('crash_console_lang');
|
||||
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||
})();
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
$bp = Auth::basePath();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Register — <?= 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);
|
||||
var l = localStorage.getItem('crash_console_lang');
|
||||
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||
})();
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
$bp = Auth::basePath();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Two-factor — <?= 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);
|
||||
var l = localStorage.getItem('crash_console_lang');
|
||||
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user