sync public allowlist from private myoffice

This commit is contained in:
myoffice-sync
2026-08-05 10:36:17 +00:00
parent 6f7fd61ee9
commit 874796c860
720 changed files with 2841 additions and 69563 deletions
+136
View File
@@ -0,0 +1,136 @@
# MyOffice on Proxmox (Gitea releases)
Three-tier setup on **your Gitea**:
| Tier | Repo | Visibility | Contents |
|------|------|------------|----------|
| 1. Dev | `myoffice` | Private | Full source |
| 2. Public code | `myoffice-public` | Public | Allowlisted scripts (`proxmox/`, `Docker/`, README) |
| 3. Published app | Releases on `myoffice-public` | Public | `myoffice-publish.tar.gz` |
CT install downloads the release tarball (not source). No Docker/nginx/SDK/Node inside the CT — proxy with your existing nginx LXC to Kestrel `:9100`.
```text
[private myoffice] --sync-public--> [myoffice-public raw scripts]
[private myoffice] --publish+release--> [Gitea Release .tar.gz]
|
[User Proxmox] curl myoffice.sh ----------> [LXC: Postgres + ASP.NET + app]
^
[User nginx] proxy_pass http://ct-ip:9100 ----+
```
## End user: create CT
```bash
export GITEA_URL="https://gitea.example.com"
export GITEA_OWNER="org"
export GITEA_REPO="myoffice-public" # optional, default
export RELEASE_TAG="latest" # or v1.2.3
export REPO_RAW_BASE="${GITEA_URL}/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/main"
bash -c "$(curl -fsSL ${REPO_RAW_BASE}/proxmox/myoffice.sh)"
```
Or copy `proxmox/` to the host and run `bash myoffice.sh` with the same env vars.
Whiptail: Default / Advanced (Public URL, release tag) → creates CT → installs Postgres + ASP.NET runtime → downloads release → configures FrontEnd/CORS + local Postgres → systemd.
## Maintainer: publish a release
On the private repo (build PC):
```powershell
$env:GITEA_TOKEN = '<token with write:repository on myoffice-public>'
.\proxmox\publish.ps1 -Tar
.\proxmox\release.ps1 -GiteaUrl https://gitea.example.com -Owner org -Repo myoffice-public -Tag v1.2.3
.\proxmox\sync-public.ps1 -GiteaUrl https://gitea.example.com -Owner org -Repo myoffice-public -Push
```
- `proxmox/publish.ps1``_Published/` + `myoffice-publish.tar.gz`
- `proxmox/release.ps1` → Gitea Release asset
- `proxmox/sync-public.ps1` → push allowlisted files to public repo (scripts users curl)
Or via Gitea Actions on the private repo (`.gitea/workflows/publish.yml`): push `master` → tag `latest`, or push tag `v*` / manual dispatch.
Lab-only SSH deploy (no Gitea): `.\proxmox\deploy.ps1 -TargetHost <ct-ip>`
## What runs inside the CT
| Service | How | Port |
|---------|-----|------|
| PostgreSQL | apt + systemd | `127.0.0.1:5432` |
| MyOffice API + SPA | `dotnet` via `myoffice-api.service` | **9100** |
```text
/opt/myoffice/api # extracted release (API + wwwroot)
/opt/myoffice/proxmox # install/update helpers + env
```
Config chain:
- **Front → API:** same-origin SPA (`environment.proxmox.ts`)
- **API → Postgres:** CT `appsettings.shared.Production.json`
- **Browser → app:** nginx → `http://<ct-ip>:9100`
## Proxy (your nginx LXC)
```nginx
location / {
proxy_pass http://<myoffice-ct-ip>:9100;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
In Advanced install, set **Public URL** to the browser URL so CORS / `FrontEnd:Host` match.
## Update later
```bash
pct enter <CTID>
/opt/myoffice/proxmox/update.sh # re-download RELEASE_TAG + restart
/opt/myoffice/proxmox/update.sh logs
/opt/myoffice/proxmox/update.sh status
```
Release settings are stored in `/opt/myoffice/proxmox/env`.
## Gitea ops checklist
- Create private `myoffice` and public `myoffice-public`
- Raise release attachment size limit if needed
- Token for maintainers: `write:repository` on public repo
- TLS (or trust custom CA) so Proxmox/`curl` can download
- Private repo secret `PUBLIC_TOKEN` for Actions (`.gitea/workflows/publish.yml`)
## Gitea runner caching
Three layers (independent):
| Layer | What | How |
|-------|------|-----|
| Toolcache | .NET SDK / Node binaries | `setup-dotnet` / `setup-node` write to `/opt/hostedtoolcache`. Persist it on the **act_runner** host so the next job reuses downloads (steps still run, but skip re-download — you already saw `Found in cache @ /opt/hostedtoolcache/...`). |
| Actions cache | NuGet + npm packages | Workflow uses `actions/cache@v4` for `~/.nuget/packages` and `~/.npm`. Needs the runner cache server (default on act_runner). |
| Skip setup | No setup steps at all | Bake SDK + Node into the job Docker image (or host runner), then set job env `SKIP_SETUP_DOTNET=true` and `SKIP_SETUP_NODE=true`. |
Example act_runner volume (compose):
```yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- act-toolcache:/opt/hostedtoolcache # persist SDK/Node between jobs
```
If jobs use ephemeral containers without that volume, SDKs download every run.
- Debian 12 LXC, unprivileged, `--onboot 1`
- 2 vCPU / 4096 MiB / 20G / `vmbr0` / DHCP
- Listen `:9100`
## Demo logins
- App: `user_UAH@user_UAH.userUAH` / `user_UAH` (also USD, EUR)
- Postgres: user/db/password `myoffice`
+138
View File
@@ -0,0 +1,138 @@
# Deploy prebuilt MyOffice artifacts to a Proxmox CT (or any Linux host).
# Usage:
# .\proxmox\publish.ps1
# .\proxmox\deploy.ps1 -TargetHost 192.168.1.120
# .\proxmox\deploy.ps1 -TargetHost 192.168.1.120 -User root -RemotePath /opt/myoffice/api
#
# Requires OpenSSH client (ssh/scp). Prefers rsync if available (Git Bash / WSL / cwRsync).
# Preserves CT-owned appsettings.Production.json / appsettings.shared.Production.json.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[Alias('ComputerName', 'Host')]
[string]$TargetHost,
[string]$User = 'root',
[Alias('Path')]
[string]$RemotePath = '/opt/myoffice/api',
[string]$Service = 'myoffice-api',
[string]$PublishDir = '',
[int]$Port = 22
)
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Resolve-Path (Join-Path $scriptDir '..')
if (-not $PublishDir) {
$PublishDir = Join-Path $repoRoot '_Published'
}
if (-not (Test-Path $PublishDir)) {
throw "Publish folder not found: $PublishDir — run .\proxmox\publish.ps1 first."
}
if (-not (Test-Path (Join-Path $PublishDir 'MyOffice.Web.dll'))) {
throw "MyOffice.Web.dll missing in $PublishDir — run .\proxmox\publish.ps1 first."
}
$ctOwnedSettings = @(
'appsettings.Production.json',
'appsettings.shared.Production.json'
)
$remote = "${User}@${TargetHost}"
$sshTarget = @('-p', "$Port", $remote)
$scpTarget = @('-P', "$Port")
function Invoke-Remote {
param([Parameter(Mandatory = $true)][string]$Command)
& ssh @sshTarget $Command
if ($LASTEXITCODE -ne 0) {
throw "ssh failed (exit $LASTEXITCODE): $Command"
}
}
function Test-Rsync {
return [bool](Get-Command rsync -ErrorAction SilentlyContinue)
}
function ConvertTo-RsyncPath([string]$WindowsPath) {
$p = ($WindowsPath.TrimEnd('\', '/') + '/') -replace '\\', '/'
if ($p -match '^[A-Za-z]:') {
$drive = $p.Substring(0, 1).ToLower()
$p = "/$drive" + $p.Substring(2)
}
return $p
}
Write-Host "-----------------------------------------------"
Write-Host "Deploy $PublishDir -> ${remote}:${RemotePath}"
Write-Host "-----------------------------------------------"
Write-Host "Stopping $Service..."
Invoke-Remote "systemctl stop $Service || true"
Invoke-Remote "mkdir -p '$RemotePath'"
$rsync = Test-Rsync
if ($rsync) {
Write-Host "Copying with rsync (preserve CT Production appsettings)..."
$rsyncSrc = ConvertTo-RsyncPath $PublishDir
$rsyncArgs = @(
'-r', '--info=progress2',
'--exclude', 'appsettings.Production.json',
'--exclude', 'appsettings.shared.Production.json',
'-e', "ssh -p $Port",
$rsyncSrc,
"${remote}:${RemotePath}/"
)
& rsync @rsyncArgs
if ($LASTEXITCODE -ne 0) {
throw "rsync failed with exit code $LASTEXITCODE"
}
}
else {
Write-Host "rsync not found — using tar+scp fallback..."
$staging = Join-Path $env:TEMP ("myoffice-deploy-" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $staging | Out-Null
try {
Get-ChildItem -Path $PublishDir -Force | Where-Object {
$_.Name -notin $ctOwnedSettings
} | ForEach-Object {
Copy-Item $_.FullName -Destination $staging -Recurse -Force
}
$tarLocal = Join-Path $env:TEMP 'myoffice-deploy.tar.gz'
if (Test-Path $tarLocal) { Remove-Item -Force $tarLocal }
Push-Location $staging
try {
tar -czf $tarLocal *
if ($LASTEXITCODE -ne 0) { throw "tar create failed" }
}
finally {
Pop-Location
}
& scp @scpTarget $tarLocal "${remote}:/tmp/myoffice-deploy.tar.gz"
if ($LASTEXITCODE -ne 0) { throw "scp failed" }
Invoke-Remote "tar -xzf /tmp/myoffice-deploy.tar.gz -C '$RemotePath' && rm -f /tmp/myoffice-deploy.tar.gz"
}
finally {
Remove-Item -Recurse -Force $staging -ErrorAction SilentlyContinue
Remove-Item -Force (Join-Path $env:TEMP 'myoffice-deploy.tar.gz') -ErrorAction SilentlyContinue
}
}
Write-Host "Starting $Service..."
Invoke-Remote "systemctl start $Service"
Invoke-Remote "systemctl --no-pager --full status $Service || true"
Write-Host ""
Write-Host "Deployed to http://${TargetHost}:9100 (or your nginx public URL)"
Write-Host ""
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# Download myoffice-publish.tar.gz from a Gitea release into API_DIR.
# Env:
# GITEA_URL, GITEA_OWNER, GITEA_REPO, RELEASE_TAG (latest|v1.2.3)
# API_DIR (default /opt/myoffice/api)
# RELEASE_ASSET_URL — optional direct override (skips API resolve)
# GITEA_TOKEN — optional for private release downloads
set -euo pipefail
API_DIR="${API_DIR:-/opt/myoffice/api}"
GITEA_URL="${GITEA_URL:-}"
GITEA_OWNER="${GITEA_OWNER:-}"
GITEA_REPO="${GITEA_REPO:-myoffice-public}"
RELEASE_TAG="${RELEASE_TAG:-latest}"
RELEASE_ASSET_URL="${RELEASE_ASSET_URL:-}"
ASSET_NAME="${ASSET_NAME:-myoffice-publish.tar.gz}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
msg() { echo -e " ==> $*"; }
curl_auth() {
if [[ -n "$GITEA_TOKEN" ]]; then
curl -fsSL -H "Authorization: token ${GITEA_TOKEN}" "$@"
else
curl -fsSL "$@"
fi
}
json_first_string() {
# Extract first "key":"value" for key=$1 from JSON on stdin (no jq/python required).
local key="$1"
tr -d '\n' | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -1
}
resolve_asset_url() {
if [[ -n "$RELEASE_ASSET_URL" ]]; then
echo "$RELEASE_ASSET_URL"
return 0
fi
if [[ -z "$GITEA_URL" || -z "$GITEA_OWNER" ]]; then
echo "Set GITEA_URL + GITEA_OWNER (and GITEA_REPO), or RELEASE_ASSET_URL." >&2
exit 1
fi
local base api_json tag browser_url
base="${GITEA_URL%/}"
if [[ "$RELEASE_TAG" == "latest" ]]; then
api_json=$(curl_auth "${base}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/latest")
tag=$(printf '%s' "$api_json" | json_first_string tag_name)
if [[ -z "$tag" ]]; then
echo "Could not resolve latest release tag from Gitea API." >&2
exit 1
fi
else
tag="$RELEASE_TAG"
api_json=$(curl_auth "${base}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${tag}" || true)
fi
# Prefer browser_download_url that ends with the asset name
browser_url=$(printf '%s' "${api_json:-}" | tr '"' '\n' | grep -F "/${ASSET_NAME}" | grep -E '^https?://' | head -1 || true)
if [[ -z "$browser_url" ]]; then
browser_url="${base}/${GITEA_OWNER}/${GITEA_REPO}/releases/download/${tag}/${ASSET_NAME}"
fi
echo "$browser_url"
}
download_and_extract() {
local url tar_path staging
url=$(resolve_asset_url)
msg "Downloading release asset: $url"
mkdir -p "$API_DIR"
tar_path="/tmp/${ASSET_NAME}"
curl_auth -o "$tar_path" "$url"
staging=$(mktemp -d /tmp/myoffice-extract.XXXXXX)
tar -xzf "$tar_path" -C "$staging"
rm -f "$tar_path"
local prod="${API_DIR}/appsettings.Production.json"
local shared_prod="${API_DIR}/appsettings.shared.Production.json"
local prod_bak="" shared_bak=""
if [[ -f "$prod" ]]; then
prod_bak=$(mktemp)
cp -a "$prod" "$prod_bak"
fi
if [[ -f "$shared_prod" ]]; then
shared_bak=$(mktemp)
cp -a "$shared_prod" "$shared_bak"
fi
local top_count top_dir
top_count=$(find "$staging" -mindepth 1 -maxdepth 1 | wc -l)
top_dir=""
if [[ "$top_count" -eq 1 ]]; then
local only
only=$(find "$staging" -mindepth 1 -maxdepth 1 | head -1)
if [[ -d "$only" ]]; then
top_dir="$only"
fi
fi
find "$API_DIR" -mindepth 1 -maxdepth 1 \
! -name 'appsettings.Production.json' \
! -name 'appsettings.shared.Production.json' \
-exec rm -rf {} +
if [[ -n "$top_dir" ]]; then
cp -a "$top_dir"/. "$API_DIR"/
else
cp -a "$staging"/. "$API_DIR"/
fi
rm -rf "$staging"
rm -f "$API_DIR/appsettings.Production.json" "$API_DIR/appsettings.shared.Production.json" 2>/dev/null || true
if [[ -n "$prod_bak" ]]; then
mv "$prod_bak" "$prod"
fi
if [[ -n "$shared_bak" ]]; then
mv "$shared_bak" "$shared_prod"
fi
if [[ ! -f "${API_DIR}/MyOffice.Web.dll" ]]; then
echo "Extracted archive but MyOffice.Web.dll is missing in ${API_DIR}" >&2
exit 1
fi
msg "App extracted to ${API_DIR}"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
download_and_extract
fi
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env bash
# Runs inside the MyOffice LXC: PostgreSQL + ASP.NET Core runtime + app from Gitea Release.
# No Docker, no nginx, no git, no SDK, no Node.
#
# Env (from proxmox/myoffice.sh):
# APP_DIR, PUBLIC_URL, API_PORT, ASSETS_DIR
# GITEA_URL, GITEA_OWNER, GITEA_REPO, RELEASE_TAG, RELEASE_ASSET_URL, GITEA_TOKEN
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/myoffice}"
API_DIR="${APP_DIR}/api"
ASSETS_DIR="${ASSETS_DIR:-/tmp/myoffice-proxmox}"
PUBLIC_URL="${PUBLIC_URL:-}"
API_PORT="${API_PORT:-9100}"
PG_DB="myoffice"
PG_USER="myoffice"
PG_PASS="myoffice"
SKIP_FETCH="${SKIP_FETCH:-0}"
export DEBIAN_FRONTEND=noninteractive
export API_DIR
msg() { echo -e " ==> $*"; }
detect_public_url() {
if [[ -n "$PUBLIC_URL" ]]; then
PUBLIC_URL=$(echo "$PUBLIC_URL" | sed 's:/*$::')
return 0
fi
local ip
ip=$(ip -4 -o addr show eth0 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1 || true)
if [[ -z "$ip" ]]; then
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
fi
if [[ -z "$ip" ]]; then
echo "Could not detect CT IP. Set PUBLIC_URL." >&2
exit 1
fi
PUBLIC_URL="http://${ip}:${API_PORT}"
}
install_base() {
msg "Installing base packages"
apt-get update -y
apt-get install -y ca-certificates curl gnupg postgresql postgresql-contrib
}
install_aspnet_runtime() {
if command -v dotnet >/dev/null 2>&1 && dotnet --list-runtimes 2>/dev/null | grep -q 'Microsoft.AspNetCore.App 10\.'; then
msg "ASP.NET Core 10 runtime already installed"
return 0
fi
msg "Installing ASP.NET Core 10 runtime"
curl -fsSL https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -o /tmp/packages-microsoft-prod.deb
dpkg -i /tmp/packages-microsoft-prod.deb
apt-get update -y
apt-get install -y aspnetcore-runtime-10.0
}
setup_postgres() {
msg "Configuring PostgreSQL"
systemctl enable --now postgresql
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${PG_USER}'" | grep -q 1; then
sudo -u postgres psql -c "CREATE USER ${PG_USER} WITH PASSWORD '${PG_PASS}';"
else
sudo -u postgres psql -c "ALTER USER ${PG_USER} WITH PASSWORD '${PG_PASS}';"
fi
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${PG_DB}'" | grep -q 1; then
sudo -u postgres psql -c "CREATE DATABASE ${PG_DB} OWNER ${PG_USER};"
fi
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${PG_DB} TO ${PG_USER};"
sudo -u postgres psql -d "$PG_DB" -c "GRANT ALL ON SCHEMA public TO ${PG_USER};" || true
msg "PostgreSQL ready (db/user/pass=${PG_DB})"
}
write_runtime_config() {
mkdir -p "$API_DIR"
if [[ ! -f "$API_DIR/appsettings.Production.json" ]]; then
cat >"$API_DIR/appsettings.Production.json" <<EOF
{
"DatabaseProvider": "npgsql",
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://*:${API_PORT}"
}
}
},
"FrontEnd": {
"Host": "${PUBLIC_URL}"
},
"Cors": {
"AllowedOrigins": [
"${PUBLIC_URL}"
]
},
"OpenIddict": {
"UseEphemeralKeys": true,
"AllowHttp": true
}
}
EOF
fi
if [[ ! -f "$API_DIR/appsettings.shared.Production.json" ]]; then
cat >"$API_DIR/appsettings.shared.Production.json" <<EOF
{
"ConnectionStrings": {
"npgsql": "Host=127.0.0.1;Port=5432;Database=${PG_DB};Username=${PG_USER};Password=${PG_PASS}"
}
}
EOF
fi
}
fetch_app() {
if [[ "$SKIP_FETCH" == "1" ]]; then
msg "SKIP_FETCH=1 — not downloading release"
return 0
fi
local fetch_script="${ASSETS_DIR}/fetch-release.sh"
if [[ ! -f "$fetch_script" ]]; then
fetch_script="${APP_DIR}/proxmox/fetch-release.sh"
fi
if [[ ! -f "$fetch_script" ]]; then
echo "Missing fetch-release.sh (looked in ASSETS_DIR and ${APP_DIR}/proxmox)." >&2
exit 1
fi
# shellcheck disable=SC1090
source "$fetch_script"
download_and_extract
}
install_systemd() {
msg "Configuring systemd unit myoffice-api"
local unit_src="${ASSETS_DIR}/myoffice-api.service"
if [[ ! -f "$unit_src" ]]; then
unit_src="${APP_DIR}/proxmox/myoffice-api.service"
fi
if [[ ! -f "$unit_src" ]]; then
echo "Missing myoffice-api.service" >&2
exit 1
fi
sed -e "s|__API_DIR__|${API_DIR}|g" -e "s|__API_PORT__|${API_PORT}|g" \
"$unit_src" >/etc/systemd/system/myoffice-api.service
mkdir -p "${APP_DIR}/proxmox"
if [[ -d "$ASSETS_DIR" ]]; then
cp -a "${ASSETS_DIR}/." "${APP_DIR}/proxmox/"
fi
chmod +x "${APP_DIR}/proxmox/"*.sh 2>/dev/null || true
cat >"${APP_DIR}/proxmox/env" <<EOF
PUBLIC_URL=${PUBLIC_URL}
API_PORT=${API_PORT}
APP_DIR=${APP_DIR}
GITEA_URL=${GITEA_URL:-}
GITEA_OWNER=${GITEA_OWNER:-}
GITEA_REPO=${GITEA_REPO:-myoffice-public}
RELEASE_TAG=${RELEASE_TAG:-latest}
RELEASE_ASSET_URL=${RELEASE_ASSET_URL:-}
EOF
systemctl daemon-reload
systemctl enable myoffice-api
if [[ -f "${API_DIR}/MyOffice.Web.dll" ]]; then
systemctl restart myoffice-api
else
systemctl stop myoffice-api 2>/dev/null || true
msg "App DLL missing — service not started"
fi
}
print_done() {
echo
echo " MyOffice CT is ready:"
echo " Public URL ${PUBLIC_URL}"
echo " Listen 0.0.0.0:${API_PORT} (proxy this from your nginx LXC)"
echo " Postgres 127.0.0.1:5432 db/user/pass=${PG_DB}"
echo " App dir ${API_DIR}"
echo
echo " Update later: ${APP_DIR}/proxmox/update.sh"
echo " Demo login: user_UAH@user_UAH.userUAH / user_UAH"
echo
}
detect_public_url
install_base
install_aspnet_runtime
setup_postgres
write_runtime_config
fetch_app
install_systemd
print_done
+19
View File
@@ -0,0 +1,19 @@
[Unit]
Description=MyOffice ASP.NET Core (API + SPA)
After=network.target postgresql.service
Wants=postgresql.service
[Service]
WorkingDirectory=__API_DIR__
ExecStart=/usr/bin/dotnet __API_DIR__/MyOffice.Web.dll
Restart=always
RestartSec=5
KillSignal=SIGINT
SyslogIdentifier=myoffice-api
User=root
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
Environment=ASPNETCORE_URLS=http://*:__API_PORT__
[Install]
WantedBy=multi-user.target
+490
View File
@@ -0,0 +1,490 @@
#!/usr/bin/env bash
# MyOffice Proxmox LXC installer (community-scripts style).
# Run on the Proxmox host (public Gitea repo raw URL):
# export GITEA_URL="https://gitea.example.com"
# export GITEA_OWNER="org"
# export GITEA_REPO="myoffice-public"
# export REPO_RAW_BASE="${GITEA_URL}/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/main"
# bash -c "$(curl -fsSL ${REPO_RAW_BASE}/proxmox/myoffice.sh)"
#
# Creates one LXC with PostgreSQL + ASP.NET runtime + app from Gitea Release.
# No Docker, no nginx, no git/SDK/Node in the CT (use your existing proxy LXC in front).
set -euo pipefail
GITEA_URL="${GITEA_URL:-}"
GITEA_OWNER="${GITEA_OWNER:-}"
GITEA_REPO="${GITEA_REPO:-myoffice-public}"
RELEASE_TAG="${RELEASE_TAG:-latest}"
RELEASE_ASSET_URL="${RELEASE_ASSET_URL:-}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
if [[ -z "${REPO_RAW_BASE:-}" ]]; then
if [[ -n "$GITEA_URL" && -n "$GITEA_OWNER" ]]; then
REPO_RAW_BASE="${GITEA_URL%/}/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/main"
else
REPO_RAW_BASE="https://gitea.example.com/OWNER/myoffice-public/raw/branch/main"
fi
fi
# Derive GITEA_* from REPO_RAW_BASE when only raw base is set:
# https://host/owner/repo/raw/branch/main
if [[ -z "$GITEA_URL" || -z "$GITEA_OWNER" ]]; then
if [[ "$REPO_RAW_BASE" =~ ^(https?://[^/]+)/([^/]+)/([^/]+)/raw/ ]]; then
[[ -z "$GITEA_URL" ]] && GITEA_URL="${BASH_REMATCH[1]}"
[[ -z "$GITEA_OWNER" ]] && GITEA_OWNER="${BASH_REMATCH[2]}"
[[ "$GITEA_REPO" == "myoffice-public" ]] && GITEA_REPO="${BASH_REMATCH[3]}"
fi
fi
APP="MyOffice"
APP_DIR="/opt/myoffice"
YW=$'\033[33m'
BL=$'\033[36m'
RD=$'\033[01;31m'
BGN=$'\033[4;92m'
GN=$'\033[1;92m'
DGN=$'\033[32m'
CL=$'\033[m'
BOLD=$'\033[1m'
BFR=$'\r\033[K'
TAB=$' '
CM="${TAB}✔️${TAB}${CL}"
CROSS="${TAB}✖️${TAB}${CL}"
INFO="${TAB}💡${TAB}${CL}"
OS="${TAB}🖥️${TAB}${CL}"
CONTAINERTYPE="${TAB}📦${TAB}${CL}"
DISKSIZE="${TAB}💾${TAB}${CL}"
CPUCORE="${TAB}🧠${TAB}${CL}"
RAMSIZE="${TAB}🛠️${TAB}${CL}"
CONTAINERID="${TAB}🆔${TAB}${CL}"
HOSTNAME="${TAB}🏠${TAB}${CL}"
BRIDGE="${TAB}🌉${TAB}${CL}"
GATEWAY="${TAB}🌐${TAB}${CL}"
DEFAULT="${TAB}⚙️${TAB}${CL}"
CREATING="${TAB}🚀${TAB}${CL}"
ADVANCED="${TAB}🧩${TAB}${CL}"
header_info() {
clear
cat <<"EOF"
__ ___ ____ ____________
/ |/ /_ __/ __ \/ __/ __/ _/______
/ /|_/ / / / / / / / /_/ /_ / // ___/ _ \
/ / / / /_/ / /_/ / __/ __// // /__/ __/
/_/ /_/\__, /\____/_/ /_/ /___/\___/\___/
/____/ Proxmox LXC (Gitea release)
EOF
}
msg_info() { echo -ne "${TAB}${YW}${1}${CL}"; }
msg_ok() { echo -e "${BFR}${CM}${GN}${1}${CL}"; }
msg_error() { echo -e "${BFR}${CROSS}${RD}${1}${CL}"; }
error_handler() {
local exit_code=$?
local line_number=$1
local command=$2
echo -e "\n${RD}[ERROR]${CL} line ${RD}${line_number}${CL}: exit ${RD}${exit_code}${CL}: ${YW}${command}${CL}\n"
if [[ -n "${CTID:-}" ]] && pct status "$CTID" &>/dev/null; then
echo -e "${INFO}CT ${CTID} exists; destroy with: pct stop ${CTID}; pct destroy ${CTID}"
fi
exit "$exit_code"
}
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
exit_script() {
clear
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
exit 0
}
check_root() {
if [[ "$(id -u)" -ne 0 ]]; then
msg_error "Please run this script as root on the Proxmox host."
exit 1
fi
}
pve_check() {
if ! command -v pveversion >/dev/null 2>&1; then
msg_error "This script must run on a Proxmox VE host (pveversion not found)."
exit 1
fi
if ! command -v whiptail >/dev/null 2>&1; then
msg_error "whiptail is required (apt install whiptail)."
exit 1
fi
}
arch_check() {
if [[ "$(dpkg --print-architecture)" != "amd64" ]]; then
msg_error "Only amd64 is supported."
exit 1
fi
}
get_valid_nextid() {
local try_id
try_id=$(pvesh get /cluster/nextid)
while true; do
if [[ -f "/etc/pve/qemu-server/${try_id}.conf" ]] || [[ -f "/etc/pve/lxc/${try_id}.conf" ]]; then
try_id=$((try_id + 1))
continue
fi
break
done
echo "$try_id"
}
select_storage() {
local STORAGE_MENU=()
local MSG_MAX_LENGTH=0
local line TAG TYPE FREE ITEM OFFSET VALID
while read -r line; do
TAG=$(echo "$line" | awk '{print $1}')
TYPE=$(echo "$line" | awk '{printf "%-10s", $2}')
FREE=$(echo "$line" | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f 2>/dev/null | awk '{printf("%9sB", $6)}' || echo "?")
ITEM=" Type: $TYPE Free: $FREE "
OFFSET=2
if [[ $((${#ITEM} + OFFSET)) -gt ${MSG_MAX_LENGTH:-0} ]]; then
MSG_MAX_LENGTH=$((${#ITEM} + OFFSET))
fi
STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content rootdir | awk 'NR>1')
VALID=$(pvesm status -content rootdir | awk 'NR>1')
if [[ -z "$VALID" ]]; then
msg_error "Unable to detect a valid storage location (rootdir)."
exit 1
elif [[ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]]; then
STORAGE=${STORAGE_MENU[0]}
else
while [[ -z "${STORAGE:+x}" ]]; do
STORAGE=$(whiptail --backtitle "MyOffice Proxmox" --title "Storage Pools" --radiolist \
"Which storage pool for ${HN}?\n(Spacebar to select)\n" \
16 $((MSG_MAX_LENGTH + 23)) 6 \
"${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit_script
done
fi
msg_ok "Using ${CL}${BL}${STORAGE}${CL}${GN} for storage."
}
default_settings() {
CTID=$(get_valid_nextid)
HN="myoffice"
CORE_COUNT="2"
RAM_SIZE="4096"
DISK_SIZE="20"
BRG="vmbr0"
NET="dhcp"
GATEWAY_IP=""
API_PORT="9100"
PUBLIC_URL="" # filled after CT IP known
START_CT="yes"
METHOD="default"
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CTID}${CL}"
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
echo -e "${OS}${BOLD}${DGN}OS: ${BGN}Debian 12${CL}"
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
echo -e "${RAMSIZE}${BOLD}${DGN}RAM: ${BGN}${RAM_SIZE} MiB${CL}"
echo -e "${DISKSIZE}${BOLD}${DGN}Disk: ${BGN}${DISK_SIZE}G${CL}"
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
echo -e "${GATEWAY}${BOLD}${DGN}IP: ${BGN}DHCP${CL}"
echo -e "${DEFAULT}${BOLD}${DGN}API listen: ${BGN}:${API_PORT}${CL} (SPA from release wwwroot)"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Stack: ${BGN}PostgreSQL + ASP.NET + Gitea release${CL}"
echo -e "${CREATING}${BOLD}${DGN}Creating MyOffice LXC with default settings${CL}"
}
advanced_settings() {
METHOD="advanced"
[[ -z "${CTID:-}" ]] && CTID=$(get_valid_nextid)
while true; do
if CTID=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Set Container ID" 8 58 "$CTID" --title "CONTAINER ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
[[ -z "$CTID" ]] && CTID=$(get_valid_nextid)
if pct status "$CTID" &>/dev/null || qm status "$CTID" &>/dev/null; then
echo -e "${CROSS}${RD}ID $CTID is already in use${CL}"
sleep 2
continue
fi
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CTID}${CL}"
break
else
exit_script
fi
done
if HN_IN=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Set Hostname" 8 58 myoffice --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
HN=$(echo "${HN_IN:-myoffice}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-' | sed 's/^-//;s/-$//' || true)
[[ -z "$HN" ]] && HN="myoffice"
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
else
exit_script
fi
while true; do
if CORE_COUNT=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
[[ -z "$CORE_COUNT" ]] && CORE_COUNT="2"
if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
break
fi
whiptail --backtitle "MyOffice Proxmox" --title "INVALID" --msgbox "CPU cores must be a positive integer." 8 58
else
exit_script
fi
done
while true; do
if RAM_SIZE=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Allocate RAM in MiB" 8 58 4096 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
[[ -z "$RAM_SIZE" ]] && RAM_SIZE="4096"
if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
echo -e "${RAMSIZE}${BOLD}${DGN}RAM: ${BGN}${RAM_SIZE} MiB${CL}"
break
fi
whiptail --backtitle "MyOffice Proxmox" --title "INVALID" --msgbox "RAM must be a positive integer (MiB)." 8 58
else
exit_script
fi
done
while true; do
if DISK_SIZE=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Disk size in GiB" 8 58 20 --title "DISK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
DISK_SIZE=$(echo "${DISK_SIZE:-20}" | tr -d ' Gg')
if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then
echo -e "${DISKSIZE}${BOLD}${DGN}Disk: ${BGN}${DISK_SIZE}G${CL}"
break
fi
whiptail --backtitle "MyOffice Proxmox" --title "INVALID" --msgbox "Disk must be a positive integer (GiB)." 8 58
else
exit_script
fi
done
if BRG=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
[[ -z "$BRG" ]] && BRG="vmbr0"
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
else
exit_script
fi
if whiptail --backtitle "MyOffice Proxmox" --title "NETWORK" --yesno "Use DHCP?" --yes-button DHCP --no-button Static 10 58; then
NET="dhcp"
GATEWAY_IP=""
echo -e "${GATEWAY}${BOLD}${DGN}IP: ${BGN}DHCP${CL}"
else
if NET=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Static IP CIDR (e.g. 192.168.1.120/24)" 8 58 --title "IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
:
else
exit_script
fi
if GATEWAY_IP=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Gateway (e.g. 192.168.1.1)" 8 58 --title "GATEWAY" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
:
else
exit_script
fi
echo -e "${GATEWAY}${BOLD}${DGN}IP: ${BGN}${NET} gw ${GATEWAY_IP}${CL}"
fi
if API_PORT=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Kestrel listen port" 8 58 9100 --title "API PORT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
[[ -z "$API_PORT" ]] && API_PORT="9100"
else
exit_script
fi
echo -e "${DEFAULT}${BOLD}${DGN}API listen: ${BGN}:${API_PORT}${CL}"
if PUBLIC_URL=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Browser public URL (empty = http://CT_IP:PORT). Use your nginx proxy URL if any." 10 70 "" --title "PUBLIC URL" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
PUBLIC_URL=$(echo "$PUBLIC_URL" | sed 's:/*$::')
else
exit_script
fi
if RELEASE_TAG=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Gitea release tag (latest or v1.2.3)" 8 58 "${RELEASE_TAG}" --title "RELEASE TAG" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
[[ -z "$RELEASE_TAG" ]] && RELEASE_TAG="latest"
else
exit_script
fi
echo -e "${DEFAULT}${BOLD}${DGN}Release: ${BGN}${RELEASE_TAG}${CL}"
if whiptail --backtitle "MyOffice Proxmox" --title "START CONTAINER" --yesno "Start CT when created?" 10 58; then
START_CT="yes"
else
START_CT="no"
fi
if whiptail --backtitle "MyOffice Proxmox" --title "ADVANCED SETTINGS" --yesno "Ready to create MyOffice LXC?" --no-button Do-Over 10 58; then
echo -e "${CREATING}${BOLD}${DGN}Creating MyOffice LXC with advanced settings${CL}"
else
header_info
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
advanced_settings
fi
}
start_script() {
if whiptail --backtitle "MyOffice Proxmox" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58; then
header_info
echo -e "${BL}Using Default Settings${CL}"
default_settings
else
header_info
echo -e "${RD}Using Advanced Settings${CL}"
advanced_settings
fi
}
ensure_template() {
local template="debian-12-standard"
local store="local"
msg_info "Ensuring Debian 12 LXC template..."
if ! pveam list "$store" 2>/dev/null | grep -q "$template"; then
pveam update >/dev/null
local remote
remote=$(pveam available -section system | awk '/debian-12-standard/ {print $2; exit}')
if [[ -z "$remote" ]]; then
msg_error "Could not find debian-12-standard template in pveam available."
exit 1
fi
pveam download "$store" "$remote"
fi
TEMPLATE_REF=$(pveam list "$store" | awk '/debian-12-standard/ {print $1; exit}')
if [[ -z "${TEMPLATE_REF:-}" ]]; then
msg_error "Debian 12 template not found after download."
exit 1
fi
msg_ok "Template ${CL}${BL}${TEMPLATE_REF}${CL}"
}
create_container() {
local net_cfg
if [[ "$NET" == "dhcp" ]]; then
net_cfg="name=eth0,bridge=${BRG},ip=dhcp"
else
net_cfg="name=eth0,bridge=${BRG},ip=${NET},gw=${GATEWAY_IP}"
fi
msg_info "Creating LXC ${CTID} (${HN})..."
pct create "$CTID" "$TEMPLATE_REF" \
--hostname "$HN" \
--cores "$CORE_COUNT" \
--memory "$RAM_SIZE" \
--swap 512 \
--rootfs "${STORAGE}:${DISK_SIZE}" \
--net0 "$net_cfg" \
--unprivileged 1 \
--ostype debian \
--onboot 1 \
--timezone host \
--tags "myoffice" \
--description "MyOffice: PostgreSQL + ASP.NET Core from Gitea release. Proxy via external nginx LXC."
msg_ok "Created CT ${CL}${BL}${CTID}${CL}"
if [[ "$START_CT" == "yes" ]]; then
msg_info "Starting CT ${CTID}..."
pct start "$CTID"
msg_ok "Started CT ${CTID}"
else
msg_error "CT created but not started (Start=no). Start with: pct start ${CTID}"
exit 0
fi
}
wait_for_network() {
msg_info "Waiting for CT network..."
local i ip
for i in $(seq 1 60); do
ip=$(pct exec "$CTID" -- bash -c "ip -4 -o addr show eth0 2>/dev/null | awk '{print \$4}' | cut -d/ -f1 | head -1" 2>/dev/null || true)
if [[ -n "$ip" ]]; then
CT_IP="$ip"
msg_ok "CT IP ${CL}${BL}${CT_IP}${CL}"
return 0
fi
sleep 2
done
msg_error "Timed out waiting for CT IP."
exit 1
}
resolve_script_dir() {
if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
else
SCRIPT_DIR=""
fi
}
push_install_assets() {
local dest_dir="/tmp/myoffice-proxmox"
pct exec "$CTID" -- mkdir -p "$dest_dir"
if [[ -n "${SCRIPT_DIR}" && -f "${SCRIPT_DIR}/install-in-ct.sh" ]]; then
for f in install-in-ct.sh fetch-release.sh myoffice-api.service update.sh; do
pct push "$CTID" "${SCRIPT_DIR}/${f}" "${dest_dir}/${f}"
done
else
msg_info "Downloading install assets from REPO_RAW_BASE..."
for f in install-in-ct.sh fetch-release.sh myoffice-api.service update.sh; do
curl -fsSL "${REPO_RAW_BASE}/proxmox/${f}" -o "/tmp/${f}"
pct push "$CTID" "/tmp/${f}" "${dest_dir}/${f}"
done
msg_ok "Downloaded install assets"
fi
pct exec "$CTID" -- chmod +x "${dest_dir}/install-in-ct.sh" "${dest_dir}/fetch-release.sh" "${dest_dir}/update.sh"
}
run_ct_install() {
if [[ -z "${PUBLIC_URL}" ]]; then
PUBLIC_URL="http://${CT_IP}:${API_PORT}"
fi
if [[ -z "${GITEA_URL}" || -z "${GITEA_OWNER}" ]] && [[ -z "${RELEASE_ASSET_URL}" ]]; then
msg_error "Set GITEA_URL + GITEA_OWNER (and GITEA_REPO), or RELEASE_ASSET_URL, before install."
exit 1
fi
msg_info "Installing PostgreSQL + ASP.NET runtime + Gitea release..."
pct exec "$CTID" -- env \
REPO_RAW_BASE="$REPO_RAW_BASE" \
APP_DIR="$APP_DIR" \
PUBLIC_URL="$PUBLIC_URL" \
API_PORT="$API_PORT" \
ASSETS_DIR="/tmp/myoffice-proxmox" \
GITEA_URL="$GITEA_URL" \
GITEA_OWNER="$GITEA_OWNER" \
GITEA_REPO="$GITEA_REPO" \
RELEASE_TAG="$RELEASE_TAG" \
RELEASE_ASSET_URL="$RELEASE_ASSET_URL" \
GITEA_TOKEN="$GITEA_TOKEN" \
bash /tmp/myoffice-proxmox/install-in-ct.sh
msg_ok "Stack installed"
}
# --- main ---
header_info
echo -e "\n Loading..."
check_root
arch_check
pve_check
if ! whiptail --backtitle "MyOffice Proxmox" --title "MyOffice LXC" --yesno \
"This will create a new LXC with:\n\n • PostgreSQL\n • ASP.NET Core runtime\n • App from Gitea Release\n\nNo Docker, no nginx (point your proxy LXC at :9100).\n\nProceed?" 16 58; then
exit_script
fi
start_script
select_storage
ensure_template
resolve_script_dir
create_container
wait_for_network
push_install_assets
run_ct_install
echo -e "\n${GN}${BOLD}Completed successfully!${CL}\n"
echo -e "${INFO}${YW}Stack in CT ${CTID}:${CL} PostgreSQL + ASP.NET + release ${RELEASE_TAG}"
echo -e "${GATEWAY}${BGN}Public URL ${PUBLIC_URL}${CL}"
echo -e "${GATEWAY}${BGN}Upstream http://${CT_IP}:${API_PORT}${CL} (for your nginx proxy)"
echo -e "${INFO}Demo login: user_UAH@user_UAH.userUAH / user_UAH"
echo -e "${INFO}Update: pct enter ${CTID} then ${APP_DIR}/proxmox/update.sh"
echo
+166
View File
@@ -0,0 +1,166 @@
# Publish MyOffice API + SPA for Proxmox (artifacts only, no source).
# Usage (from repo root):
# .\proxmox\publish.ps1
# .\proxmox\publish.ps1 -OutDir D:\builds\myoffice
# .\proxmox\publish.ps1 -Tar # also write myoffice-publish.tar.gz next to OutDir
#
# Before build:
# 1) copy appsettings.shared.json → Development + Production overlays (only if missing)
# 2) restore NuGet + npm
#
# Output: <OutDir>/ with MyOffice.Web.dll + wwwroot/ (SPA, same-origin URLs)
[CmdletBinding()]
param(
[string]$OutDir = '',
[switch]$Tar
)
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Resolve-Path (Join-Path $scriptDir '..')
Set-Location $repoRoot
if (-not $OutDir) {
$OutDir = Join-Path $repoRoot '_Published'
}
$OutDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutDir)
$sharedDir = Join-Path $repoRoot 'MyOffice.Shared'
$sharedBase = Join-Path $sharedDir 'appsettings.shared.json'
if (-not (Test-Path $sharedBase)) {
throw "Missing $sharedBase"
}
Write-Host "-----------------------------------------------"
Write-Host "Copy appsettings.shared.json -> Development + Production (if missing)"
Write-Host "-----------------------------------------------"
foreach ($name in @('appsettings.shared.Development.json', 'appsettings.shared.Production.json')) {
$dest = Join-Path $sharedDir $name
if (Test-Path $dest) {
Write-Host "Skip (exists): $name"
}
else {
Copy-Item $sharedBase $dest
Write-Host "Created: $name"
}
}
Write-Host "-----------------------------------------------"
Write-Host "Restore NuGet packages"
Write-Host "-----------------------------------------------"
dotnet restore 'MyOffice.Web\MyOffice.Web.csproj' --nologo
if ($LASTEXITCODE -ne 0) {
throw "dotnet restore failed with exit code $LASTEXITCODE"
}
$spaDir = Join-Path $repoRoot 'MyOffice.SPA'
$envSample = Join-Path $spaDir 'src\environments\environment.sample.ts'
$envLocal = Join-Path $spaDir 'src\environments\environment.ts'
if (-not (Test-Path $envLocal) -and (Test-Path $envSample)) {
Copy-Item $envSample $envLocal
}
Write-Host "-----------------------------------------------"
Write-Host "Restore npm packages (MyOffice.SPA)"
Write-Host "-----------------------------------------------"
Push-Location $spaDir
try {
if (Test-Path (Join-Path $spaDir 'package-lock.json')) {
npm ci
}
else {
npm install
}
if ($LASTEXITCODE -ne 0) {
throw "npm restore failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
Write-Host "-----------------------------------------------"
Write-Host "dotnet publish -> $OutDir"
Write-Host "-----------------------------------------------"
if (Test-Path $OutDir) {
Remove-Item -Recurse -Force $OutDir
}
New-Item -ItemType Directory -Path $OutDir | Out-Null
dotnet publish 'MyOffice.Web\MyOffice.Web.csproj' -c Release -o $OutDir --no-restore --nologo
if ($LASTEXITCODE -ne 0) {
throw "dotnet publish failed with exit code $LASTEXITCODE"
}
# CT owns Production overlays — do not ship local env-specific appsettings.
# Keep appsettings.json + appsettings.shared.json (required at runtime).
Get-ChildItem -Path $OutDir -Filter 'appsettings*.json' -File -ErrorAction SilentlyContinue |
Where-Object {
$_.Name -notin @('appsettings.json', 'appsettings.shared.json')
} |
Remove-Item -Force
Write-Host "-----------------------------------------------"
Write-Host "ng build (proxmox) -> $OutDir\wwwroot"
Write-Host "-----------------------------------------------"
Push-Location $spaDir
try {
$spaOut = Join-Path $env:TEMP ("myoffice-spa-" + [guid]::NewGuid().ToString('N'))
npx ng build --configuration proxmox --output-path $spaOut
if ($LASTEXITCODE -ne 0) {
throw "ng build failed with exit code $LASTEXITCODE"
}
$wwwroot = Join-Path $OutDir 'wwwroot'
if (Test-Path $wwwroot) {
Remove-Item -Recurse -Force $wwwroot
}
New-Item -ItemType Directory -Path $wwwroot | Out-Null
$browser = Join-Path $spaOut 'browser'
if (Test-Path $browser) {
Copy-Item -Path (Join-Path $browser '*') -Destination $wwwroot -Recurse -Force
}
else {
Copy-Item -Path (Join-Path $spaOut '*') -Destination $wwwroot -Recurse -Force
}
Remove-Item -Recurse -Force $spaOut -ErrorAction SilentlyContinue
}
finally {
Pop-Location
}
if ($Tar) {
$tarPath = Join-Path (Split-Path -Parent $OutDir) 'myoffice-publish.tar.gz'
if (Test-Path $tarPath) {
Remove-Item -Force $tarPath
}
Write-Host "-----------------------------------------------"
Write-Host "tar -> $tarPath"
Write-Host "-----------------------------------------------"
# Windows 10+ tar; paths relative to OutDir parent
Push-Location (Split-Path -Parent $OutDir)
try {
$leaf = Split-Path -Leaf $OutDir
tar -czf $tarPath $leaf
if ($LASTEXITCODE -ne 0) {
throw "tar failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
Write-Host "Archive: $tarPath"
}
Write-Host ""
Write-Host "Published to $OutDir"
Write-Host "Next (Gitea): .\proxmox\publish.ps1 -Tar ; .\proxmox\release.ps1 -GiteaUrl ... -Owner ... -Tag v1.2.3"
Write-Host "Linux/CI: ./proxmox/publish.sh --tar ; ./proxmox/release.sh ..."
Write-Host "Lab SSH: .\proxmox\deploy.ps1 -TargetHost <ct-ip>"
Write-Host ""
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# Publish MyOffice API + SPA for Proxmox (artifacts only, no source).
# Usage (from repo root):
# ./proxmox/publish.sh
# ./proxmox/publish.sh --out-dir /tmp/myoffice-out
# ./proxmox/publish.sh --tar
#
# Output: <out-dir>/ with MyOffice.Web.dll + wwwroot/ (SPA, same-origin URLs)
set -euo pipefail
script_dir="$(cd "$(dirname "$0")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
cd "$repo_root"
out_dir="${OUT_DIR:-_Published}"
do_tar=false
while [[ $# -gt 0 ]]; do
case "$1" in
--tar | -Tar)
do_tar=true
shift
;;
--out-dir)
out_dir="$2"
shift 2
;;
-h | --help)
echo "Usage: $0 [--tar] [--out-dir DIR]"
exit 0
;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
done
# Resolve relative out_dir against repo root
if [[ "$out_dir" != /* ]]; then
out_dir="$repo_root/$out_dir"
fi
shared_dir="$repo_root/MyOffice.Shared"
shared_base="$shared_dir/appsettings.shared.json"
if [[ ! -f "$shared_base" ]]; then
echo "Missing $shared_base" >&2
exit 1
fi
echo "-----------------------------------------------"
echo "Copy appsettings.shared.json -> Development + Production (if missing)"
echo "-----------------------------------------------"
for name in appsettings.shared.Development.json appsettings.shared.Production.json; do
dest="$shared_dir/$name"
if [[ -f "$dest" ]]; then
echo "Skip (exists): $name"
else
cp "$shared_base" "$dest"
echo "Created: $name"
fi
done
echo "-----------------------------------------------"
echo "Restore NuGet packages"
echo "-----------------------------------------------"
dotnet restore MyOffice.Web/MyOffice.Web.csproj --nologo
spa_dir="$repo_root/MyOffice.SPA"
env_sample="$spa_dir/src/environments/environment.sample.ts"
env_local="$spa_dir/src/environments/environment.ts"
if [[ ! -f "$env_local" && -f "$env_sample" ]]; then
cp "$env_sample" "$env_local"
fi
echo "-----------------------------------------------"
echo "Restore npm packages (MyOffice.SPA)"
echo "-----------------------------------------------"
pushd "$spa_dir" >/dev/null
if [[ -f package-lock.json ]]; then
npm ci
else
npm install
fi
popd >/dev/null
echo "-----------------------------------------------"
echo "dotnet publish -> $out_dir"
echo "-----------------------------------------------"
rm -rf "$out_dir"
mkdir -p "$out_dir"
dotnet publish MyOffice.Web/MyOffice.Web.csproj -c Release -o "$out_dir" --no-restore --nologo
# CT owns Production overlays — do not ship local env-specific appsettings.
while IFS= read -r -d '' f; do
base="$(basename "$f")"
if [[ "$base" != appsettings.json && "$base" != appsettings.shared.json ]]; then
rm -f "$f"
fi
done < <(find "$out_dir" -maxdepth 1 -name 'appsettings*.json' -print0)
echo "-----------------------------------------------"
echo "ng build (proxmox) -> $out_dir/wwwroot"
echo "-----------------------------------------------"
spa_out="$(mktemp -d /tmp/myoffice-spa.XXXXXX)"
pushd "$spa_dir" >/dev/null
npx ng build --configuration proxmox --output-path "$spa_out"
popd >/dev/null
wwwroot="$out_dir/wwwroot"
rm -rf "$wwwroot"
mkdir -p "$wwwroot"
if [[ -d "$spa_out/browser" ]]; then
cp -a "$spa_out/browser/." "$wwwroot/"
else
cp -a "$spa_out/." "$wwwroot/"
fi
rm -rf "$spa_out"
if [[ "$do_tar" == true ]]; then
out_parent="$(cd "$(dirname "$out_dir")" && pwd)"
out_leaf="$(basename "$out_dir")"
tar_path="$out_parent/myoffice-publish.tar.gz"
rm -f "$tar_path"
echo "-----------------------------------------------"
echo "tar -> $tar_path"
echo "-----------------------------------------------"
pushd "$out_parent" >/dev/null
tar -czf "$tar_path" "$out_leaf"
popd >/dev/null
echo "Archive: $tar_path"
fi
echo ""
echo "Published to $out_dir"
echo "Next (Gitea): ./proxmox/publish.sh --tar ; ./proxmox/release.sh ..."
echo "Lab SSH: ./proxmox/deploy.ps1 -TargetHost <ct-ip>"
echo ""
+154
View File
@@ -0,0 +1,154 @@
# Upload myoffice-publish.tar.gz to a Gitea Release on the public repo.
# Usage:
# $env:GITEA_TOKEN = '<token>'
# .\proxmox\publish.ps1 -Tar
# .\proxmox\release.ps1 -GiteaUrl https://gitea.example.com -Owner org -Repo myoffice-public -Tag v1.2.3
#
# Token needs write:repository on the public repo.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$GiteaUrl,
[Parameter(Mandatory = $true)]
[string]$Owner,
[string]$Repo = 'myoffice-public',
[Parameter(Mandatory = $true)]
[string]$Tag,
[string]$Token = $env:GITEA_TOKEN,
[string]$AssetPath = '',
[string]$Title = '',
[string]$Body = 'MyOffice published API + SPA (Proxmox CT artifact).',
[string]$Target = 'main'
)
$ErrorActionPreference = 'Stop'
if (-not $Token) {
throw 'GITEA_TOKEN (or -Token) is required.'
}
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Resolve-Path (Join-Path $scriptDir '..')
if (-not $AssetPath) {
$AssetPath = Join-Path $repoRoot 'myoffice-publish.tar.gz'
}
if (-not (Test-Path $AssetPath)) {
throw "Asset not found: $AssetPath — run .\proxmox\publish.ps1 -Tar first."
}
$GiteaUrl = $GiteaUrl.TrimEnd('/')
$api = "$GiteaUrl/api/v1"
$assetName = 'myoffice-publish.tar.gz'
if (-not $Title) { $Title = $Tag }
$headers = @{
Authorization = "token $Token"
Accept = 'application/json'
}
function Invoke-GiteaJson {
param(
[string]$Method,
[string]$Uri,
[object]$BodyObject = $null
)
$params = @{
Method = $Method
Uri = $Uri
Headers = $headers
}
if ($null -ne $BodyObject) {
$params.ContentType = 'application/json'
$params.Body = ($BodyObject | ConvertTo-Json -Depth 6)
}
return Invoke-RestMethod @params
}
Write-Host "-----------------------------------------------"
Write-Host "Gitea release $Owner/$Repo tag $Tag"
Write-Host "-----------------------------------------------"
$release = $null
try {
$release = Invoke-GiteaJson -Method GET -Uri "$api/repos/$Owner/$Repo/releases/tags/$Tag"
Write-Host "Release $Tag already exists (id $($release.id))."
}
catch {
Write-Host "Creating release $Tag..."
$release = Invoke-GiteaJson -Method POST -Uri "$api/repos/$Owner/$Repo/releases" -BodyObject @{
tag_name = $Tag
target_commitish = $Target
name = $Title
body = $Body
draft = $false
prerelease = $false
}
Write-Host "Created release id $($release.id)."
}
# Remove existing asset with the same name so re-upload works
$existing = @($release.assets) | Where-Object { $_.name -eq $assetName }
foreach ($a in $existing) {
Write-Host "Deleting existing asset id $($a.id) ($($a.name))..."
Invoke-GiteaJson -Method DELETE -Uri "$api/repos/$Owner/$Repo/releases/$($release.id)/assets/$($a.id)" | Out-Null
}
$uploadUrl = "$api/repos/$Owner/$Repo/releases/$($release.id)/assets?name=$assetName"
Write-Host "Uploading $AssetPath ..."
# Prefer curl multipart (reliable on Windows PowerShell 5 + 7)
$curl = Get-Command curl.exe -ErrorAction SilentlyContinue
if ($curl) {
$args = @(
'-fsS', '-X', 'POST',
'-H', "Authorization: token $Token",
'-F', "attachment=@$AssetPath",
$uploadUrl
)
& curl.exe @args
if ($LASTEXITCODE -ne 0) {
throw "curl upload failed with exit code $LASTEXITCODE"
}
}
else {
# Fallback: .NET multipart
Add-Type -AssemblyName System.Net.Http
$handler = [System.Net.Http.HttpClientHandler]::new()
$client = [System.Net.Http.HttpClient]::new($handler)
$client.DefaultRequestHeaders.Add('Authorization', "token $Token")
$content = [System.Net.Http.MultipartFormDataContent]::new()
$fs = [System.IO.File]::OpenRead($AssetPath)
try {
$streamContent = [System.Net.Http.StreamContent]::new($fs)
$streamContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/octet-stream')
$content.Add($streamContent, 'attachment', $assetName)
$resp = $client.PostAsync($uploadUrl, $content).Result
if (-not $resp.IsSuccessStatusCode) {
$err = $resp.Content.ReadAsStringAsync().Result
throw "Upload failed: $($resp.StatusCode) $err"
}
}
finally {
$fs.Dispose()
$content.Dispose()
$client.Dispose()
}
}
$downloadUrl = "$GiteaUrl/$Owner/$Repo/releases/download/$Tag/$assetName"
Write-Host ""
Write-Host "Uploaded. Download URL:"
Write-Host " $downloadUrl"
Write-Host "Latest API:"
Write-Host " $api/repos/$Owner/$Repo/releases/latest"
Write-Host ""
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# Upload myoffice-publish.tar.gz to a Gitea Release on the public repo.
# Usage:
# export GITEA_TOKEN='<token>'
# ./proxmox/publish.sh --tar
# ./proxmox/release.sh --gitea-url https://gitea.example.com --owner org --repo myoffice-public --tag v1.2.3
#
# Token needs write:repository on the public repo.
set -euo pipefail
script_dir="$(cd "$(dirname "$0")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
gitea_url="${GITEA_URL:-}"
owner="${GITEA_OWNER:-}"
repo="${GITEA_REPO:-myoffice-public}"
tag="${TAG:-}"
token="${GITEA_TOKEN:-}"
asset_path="${ASSET_PATH:-}"
title="${TITLE:-}"
body="${BODY:-MyOffice published API + SPA (Proxmox CT artifact).}"
target="${TARGET:-main}"
usage() {
echo "Usage: $0 --gitea-url URL --owner OWNER --tag TAG [--repo REPO] [--asset-path PATH]" >&2
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--gitea-url)
gitea_url="$2"
shift 2
;;
--owner)
owner="$2"
shift 2
;;
--repo)
repo="$2"
shift 2
;;
--tag)
tag="$2"
shift 2
;;
--asset-path)
asset_path="$2"
shift 2
;;
--title)
title="$2"
shift 2
;;
--body)
body="$2"
shift 2
;;
--target)
target="$2"
shift 2
;;
-h | --help)
usage
;;
*)
echo "Unknown argument: $1" >&2
usage
;;
esac
done
if [[ -z "$token" ]]; then
echo "GITEA_TOKEN (or export before run) is required." >&2
exit 1
fi
if [[ -z "$gitea_url" || -z "$owner" || -z "$tag" ]]; then
usage
fi
if [[ -z "$asset_path" ]]; then
asset_path="$repo_root/myoffice-publish.tar.gz"
fi
if [[ ! -f "$asset_path" ]]; then
echo "Asset not found: $asset_path — run ./proxmox/publish.sh --tar first." >&2
exit 1
fi
gitea_url="${gitea_url%/}"
api="$gitea_url/api/v1"
asset_name="myoffice-publish.tar.gz"
if [[ -z "$title" ]]; then
title="$tag"
fi
auth_header="Authorization: token $token"
api_call() {
local method="$1"
local uri="$2"
local payload="${3:-}"
if [[ -n "$payload" ]]; then
curl -fsS -X "$method" -H "$auth_header" -H "Accept: application/json" \
-H "Content-Type: application/json" -d "$payload" "$uri"
else
curl -fsS -X "$method" -H "$auth_header" -H "Accept: application/json" "$uri"
fi
}
release_json=""
if release_json="$(api_call GET "$api/repos/$owner/$repo/releases/tags/$tag" 2>/dev/null)"; then
release_id="$(python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" <<<"$release_json")"
echo "Release $tag already exists (id $release_id)."
else
echo "Creating release $tag..."
payload="$(TAG="$tag" TITLE="$title" BODY="$body" TARGET="$target" python3 <<'PY'
import json, os
print(json.dumps({
"tag_name": os.environ["TAG"],
"target_commitish": os.environ["TARGET"],
"name": os.environ["TITLE"],
"body": os.environ["BODY"],
"draft": False,
"prerelease": False,
}))
PY
)"
release_json="$(api_call POST "$api/repos/$owner/$repo/releases" "$payload")"
release_id="$(python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" <<<"$release_json")"
echo "Created release id $release_id."
fi
asset_ids="$(python3 -c "
import json, sys
data = json.load(sys.stdin)
for a in data.get('assets', []):
if a.get('name') == '$asset_name':
print(a['id'])
" <<<"$release_json")"
while IFS= read -r asset_id; do
[[ -z "$asset_id" ]] && continue
echo "Deleting existing asset id $asset_id ($asset_name)..."
api_call DELETE "$api/repos/$owner/$repo/releases/$release_id/assets/$asset_id" >/dev/null || true
done <<<"$asset_ids"
upload_url="$api/repos/$owner/$repo/releases/$release_id/assets?name=$asset_name"
echo "Uploading $asset_path ..."
curl -fsS -X POST -H "$auth_header" -F "attachment=@$asset_path" "$upload_url"
download_url="$gitea_url/$owner/$repo/releases/download/$tag/$asset_name"
echo ""
echo "Uploaded. Download URL:"
echo " $download_url"
echo "Latest API:"
echo " $api/repos/$owner/$repo/releases/latest"
echo ""
+161
View File
@@ -0,0 +1,161 @@
# Sync allowlisted paths from the private repo to the public Gitea repo (no full source).
# Usage:
# $env:GITEA_TOKEN = '<token>' # optional if using SSH remote
# .\proxmox\sync-public.ps1 -GiteaUrl https://gitea.example.com -Owner org -Repo myoffice-public -Push
#
# Allowlist: proxmox/, Docker/, README.md, LICENSE (if present).
# Never syncs MyOffice.* source, _Published, secrets, node_modules, etc.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$GiteaUrl,
[Parameter(Mandatory = $true)]
[string]$Owner,
[string]$Repo = 'myoffice-public',
[string]$Branch = 'main',
[string]$Token = $env:GITEA_TOKEN,
[string]$WorkDir = '',
[switch]$Push
)
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Resolve-Path (Join-Path $scriptDir '..')
Set-Location $repoRoot
$GiteaUrl = $GiteaUrl.TrimEnd('/')
if (-not $WorkDir) {
$WorkDir = Join-Path $env:TEMP 'myoffice-public-sync'
}
$allowDirs = @('proxmox', 'Docker')
$allowFiles = @('README.md', 'LICENSE', 'LICENSE.md')
$excludeNames = @('.git', '_Published', 'myoffice-publish.tar.gz', 'node_modules', 'data', '.env', 'env.local')
function Test-Excluded([string]$Name) {
return $excludeNames -contains $Name
}
function Copy-Allowlisted {
param([string]$DestRoot)
foreach ($dir in $allowDirs) {
$src = Join-Path $repoRoot $dir
if (-not (Test-Path $src)) {
Write-Host "Skip missing dir: $dir"
continue
}
$dest = Join-Path $DestRoot $dir
if (Test-Path $dest) {
Remove-Item -Recurse -Force $dest
}
New-Item -ItemType Directory -Path $dest -Force | Out-Null
Get-ChildItem -Path $src -Force | Where-Object { -not (Test-Excluded $_.Name) } | ForEach-Object {
if ($dir -eq 'Docker' -and $_.Name -eq 'data') { return }
Copy-Item $_.FullName -Destination (Join-Path $dest $_.Name) -Recurse -Force
}
Write-Host "Synced $dir/"
}
foreach ($file in $allowFiles) {
$src = Join-Path $repoRoot $file
if (Test-Path $src) {
Copy-Item $src -Destination (Join-Path $DestRoot $file) -Force
Write-Host "Synced $file"
}
}
$publicNote = Join-Path $DestRoot 'PUBLIC.md'
@(
'# MyOffice public tree',
'',
'This repository is a **filtered** mirror for Proxmox CT install scripts and Docker demo files.',
'Application **source** stays in the private developer repository.',
'Runnable app binaries are published as **Gitea Releases** (`myoffice-publish.tar.gz`).',
'',
'See [proxmox/README.md](proxmox/README.md).',
''
) | Set-Content -Path $publicNote -Encoding utf8
}
if ($Token) {
$hostPart = $GiteaUrl -replace '^https://', '' -replace '^http://', ''
$scheme = if ($GiteaUrl -match '^http://') { 'http' } else { 'https' }
$remote = "${scheme}://oauth2:${Token}@${hostPart}/${Owner}/${Repo}.git"
}
else {
$remote = "${GiteaUrl}/${Owner}/${Repo}.git"
Write-Host "No token — using remote as-is (SSH URL recommended): $remote"
}
Write-Host "-----------------------------------------------"
Write-Host "Sync allowlist -> $Owner/$Repo ($Branch)"
Write-Host "WorkDir: $WorkDir"
Write-Host "-----------------------------------------------"
if (Test-Path $WorkDir) {
Remove-Item -Recurse -Force $WorkDir
}
New-Item -ItemType Directory -Path $WorkDir | Out-Null
$cloned = $false
git clone --depth 1 --branch $Branch $remote $WorkDir 2>$null
if ($LASTEXITCODE -eq 0) {
$cloned = $true
}
if (-not $cloned) {
Write-Host "Clone failed or empty repo — initializing new git repo..."
git -C $WorkDir init -b $Branch
if ($LASTEXITCODE -ne 0) {
git -C $WorkDir init
git -C $WorkDir checkout -b $Branch 2>$null
}
git -C $WorkDir remote remove origin 2>$null
git -C $WorkDir remote add origin $remote
}
Get-ChildItem -Path $WorkDir -Force | Where-Object { $_.Name -ne '.git' } | ForEach-Object {
Remove-Item $_.FullName -Recurse -Force
}
Copy-Allowlisted -DestRoot $WorkDir
git -C $WorkDir add -A
$status = git -C $WorkDir status --porcelain
if (-not $status) {
Write-Host "No changes to commit."
}
else {
git -C $WorkDir -c user.email='myoffice-sync@local' -c user.name='myoffice-sync' `
commit -m "sync public allowlist from private myoffice"
Write-Host "Committed allowlist snapshot."
}
if ($Push) {
Write-Host "Pushing to origin $Branch..."
git -C $WorkDir push -u origin "HEAD:$Branch"
if ($LASTEXITCODE -ne 0) {
throw "git push failed with exit code $LASTEXITCODE"
}
Write-Host "Pushed."
}
else {
Write-Host "Dry run (no push). Re-run with -Push to publish."
Write-Host "Work tree left at: $WorkDir"
}
Write-Host ""
Write-Host "Raw base for CT install:"
Write-Host " $GiteaUrl/$Owner/$Repo/raw/branch/$Branch"
Write-Host ""
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# Sync allowlisted paths from the private repo to the public Gitea repo (no full source).
# Usage:
# export GITEA_TOKEN='<token>'
# ./proxmox/sync-public.sh --gitea-url https://gitea.example.com --owner org --repo myoffice-public --push
#
# Allowlist: proxmox/, Docker/, README.md, LICENSE (if present).
set -euo pipefail
script_dir="$(cd "$(dirname "$0")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
cd "$repo_root"
gitea_url="${GITEA_URL:-}"
owner="${GITEA_OWNER:-}"
repo="${GITEA_REPO:-myoffice-public}"
branch="${BRANCH:-main}"
token="${GITEA_TOKEN:-}"
work_dir="${WORK_DIR:-}"
do_push=false
usage() {
echo "Usage: $0 --gitea-url URL --owner OWNER [--repo REPO] [--branch BRANCH] [--push]" >&2
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--gitea-url)
gitea_url="$2"
shift 2
;;
--owner)
owner="$2"
shift 2
;;
--repo)
repo="$2"
shift 2
;;
--branch)
branch="$2"
shift 2
;;
--work-dir)
work_dir="$2"
shift 2
;;
--push)
do_push=true
shift
;;
-h | --help)
usage
;;
*)
echo "Unknown argument: $1" >&2
usage
;;
esac
done
if [[ -z "$gitea_url" || -z "$owner" ]]; then
usage
fi
gitea_url="${gitea_url%/}"
if [[ -z "$work_dir" ]]; then
work_dir="${TMPDIR:-/tmp}/myoffice-public-sync"
fi
allow_dirs=(proxmox Docker)
allow_files=(README.md LICENSE LICENSE.md)
exclude_names=('.git' '_Published' 'myoffice-publish.tar.gz' 'node_modules' 'data' '.env' 'env.local')
is_excluded() {
local name="$1"
for x in "${exclude_names[@]}"; do
[[ "$name" == "$x" ]] && return 0
done
return 1
}
if [[ -n "$token" ]]; then
host_part="${gitea_url#https://}"
host_part="${host_part#http://}"
if [[ "$gitea_url" == http://* ]]; then
scheme=http
else
scheme=https
fi
remote="${scheme}://oauth2:${token}@${host_part}/${owner}/${repo}.git"
else
remote="${gitea_url}/${owner}/${repo}.git"
echo "No token — using remote as-is (SSH URL recommended): $remote"
fi
echo "-----------------------------------------------"
echo "Sync allowlist -> $owner/$repo ($branch)"
echo "WorkDir: $work_dir"
echo "-----------------------------------------------"
rm -rf "$work_dir"
mkdir -p "$work_dir"
if git clone --depth 1 --branch "$branch" "$remote" "$work_dir" 2>/dev/null; then
:
else
echo "Clone failed or empty repo — initializing new git repo..."
git -C "$work_dir" init -b "$branch" 2>/dev/null || {
git -C "$work_dir" init
git -C "$work_dir" checkout -b "$branch" 2>/dev/null || true
}
git -C "$work_dir" remote remove origin 2>/dev/null || true
git -C "$work_dir" remote add origin "$remote"
fi
find "$work_dir" -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
for dir in "${allow_dirs[@]}"; do
src="$repo_root/$dir"
if [[ ! -d "$src" ]]; then
echo "Skip missing dir: $dir"
continue
fi
dest="$work_dir/$dir"
mkdir -p "$dest"
while IFS= read -r -d '' item; do
name="$(basename "$item")"
if is_excluded "$name"; then
continue
fi
if [[ "$dir" == Docker && "$name" == data ]]; then
continue
fi
cp -a "$item" "$dest/"
done < <(find "$src" -mindepth 1 -maxdepth 1 -print0)
echo "Synced $dir/"
done
for file in "${allow_files[@]}"; do
src="$repo_root/$file"
if [[ -f "$src" ]]; then
cp -a "$src" "$work_dir/$file"
echo "Synced $file"
fi
done
cat >"$work_dir/PUBLIC.md" <<'EOF'
# MyOffice public tree
This repository is a **filtered** mirror for Proxmox CT install scripts and Docker demo files.
Application **source** stays in the private developer repository.
Runnable app binaries are published as **Gitea Releases** (`myoffice-publish.tar.gz`).
See [proxmox/README.md](proxmox/README.md).
EOF
git -C "$work_dir" add -A
if [[ -z "$(git -C "$work_dir" status --porcelain)" ]]; then
echo "No changes to commit."
else
git -C "$work_dir" -c user.email='myoffice-sync@local' -c user.name='myoffice-sync' \
commit -m "sync public allowlist from private myoffice"
echo "Committed allowlist snapshot."
fi
if [[ "$do_push" == true ]]; then
echo "Pushing to origin $branch..."
git -C "$work_dir" push -u origin "HEAD:$branch"
echo "Pushed."
else
echo "Dry run (no push). Re-run with --push to publish."
echo "Work tree left at: $work_dir"
fi
echo ""
echo "Raw base for CT install:"
echo " $gitea_url/$owner/$repo/raw/branch/$branch"
echo ""
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Helpers inside an existing MyOffice LXC (runtime + Postgres + Gitea release app).
# Usage:
# /opt/myoffice/proxmox/update.sh # download latest/tag release + restart
# /opt/myoffice/proxmox/update.sh upgrade
# /opt/myoffice/proxmox/update.sh logs
# /opt/myoffice/proxmox/update.sh restart
# /opt/myoffice/proxmox/update.sh status
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
API_DIR="${APP_DIR}/api"
ACTION="${1:-upgrade}"
# shellcheck disable=SC1091
[[ -f "${SCRIPT_DIR}/env" ]] && . "${SCRIPT_DIR}/env"
export APP_DIR API_DIR
export GITEA_URL="${GITEA_URL:-}"
export GITEA_OWNER="${GITEA_OWNER:-}"
export GITEA_REPO="${GITEA_REPO:-myoffice-public}"
export RELEASE_TAG="${RELEASE_TAG:-latest}"
export RELEASE_ASSET_URL="${RELEASE_ASSET_URL:-}"
export GITEA_TOKEN="${GITEA_TOKEN:-}"
upgrade_app() {
systemctl stop myoffice-api 2>/dev/null || true
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/fetch-release.sh"
download_and_extract
systemctl start myoffice-api
systemctl status myoffice-api --no-pager || true
}
case "$ACTION" in
upgrade | update | rebuild | up)
upgrade_app
;;
logs)
journalctl -u myoffice-api -f
;;
restart)
systemctl restart myoffice-api
systemctl status myoffice-api --no-pager
;;
status)
systemctl status myoffice-api --no-pager || true
if [[ ! -f "${API_DIR}/MyOffice.Web.dll" ]]; then
echo
echo "No app binaries. Run: $0 upgrade"
echo "Or set GITEA_URL / GITEA_OWNER / RELEASE_TAG in ${SCRIPT_DIR}/env"
fi
;;
*)
echo "Usage: $0 [upgrade|status|logs|restart]" >&2
exit 1
;;
esac