sync public allowlist from private myoffice
This commit is contained in:
@@ -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`
|
||||
@@ -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 ""
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ""
|
||||
Executable
+141
@@ -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 ""
|
||||
@@ -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 ""
|
||||
Executable
+158
@@ -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 ""
|
||||
@@ -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 ""
|
||||
Executable
+183
@@ -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 ""
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user