diff --git a/README.md b/README.md index 837d35b1..0a665f79 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,16 @@ Re-running the import adds new poems and leaves existing ones untouched, so it c git pull && docker compose up -d --build ``` +## Run locally (macOS/Linux) + +```sh +./run-local.sh import # SQL Server container + API + site, then imports diwan-data (~1 h, background) +./run-local.sh # later runs: rebuild + start +./run-local.sh stop +``` + +Site: http://localhost:5200 · API: http://localhost:5100/swagger · admin `admin@diwan.local` / `Test!123`. On Apple Silicon, start Docker via `colima start --vm-type vz --vz-rosetta --memory 6` first (SQL Server is x86-64 only). + ## Build locally (macOS/Linux) ```sh diff --git a/run-local.sh b/run-local.sh new file mode 100755 index 00000000..505983bc --- /dev/null +++ b/run-local.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Run the full Diwan app locally (macOS/Linux): SQL Server in Docker + API + site, natively with dotnet. +# ./run-local.sh start everything (site http://localhost:5200, API http://localhost:5100) +# ./run-local.sh import also import diwan-data (from $DIWAN_DATA, default ../diwan, else the public CDN) +# ./run-local.sh stop stop API + site (SQL Server container keeps running) +# Needs: docker (on Apple Silicon: colima start --vm-type vz --vz-rosetta --memory 6), .NET SDK 10.0.302, python3. +# First admin: admin@diwan.local / Test!123 (change it in the user panel). +set -euo pipefail +cd "$(dirname "$0")" +ROOT=$PWD +RUN=${DIWAN_LOCAL:-$HOME/diwan-local} +SA_PASSWORD=${MSSQL_SA_PASSWORD:-Diwan_local_2026!} +DATA=${DIWAN_DATA:-$ROOT/../diwan} +export PATH=$HOME/.dotnet:$PATH DOTNET_CLI_TELEMETRY_OPTOUT=1 +mkdir -p "$RUN"/{logs,keys,museum,museum-trash,tempaudio,audio,export} + +stop() { pkill -f "dotnet RMuseum.dll" || true; pkill -f "dotnet DiwanRazor.dll" || true; } +if [ "${1:-}" = stop ]; then stop; echo stopped; exit 0; fi + +# 1. SQL Server (x86-64 image; runs under Rosetta on Apple Silicon) +if ! docker ps --format '{{.Names}}' | grep -qx diwan-mssql; then + docker start diwan-mssql 2>/dev/null || docker run -d --name diwan-mssql --platform linux/amd64 \ + -e ACCEPT_EULA=Y -e MSSQL_PID=Express -e "MSSQL_SA_PASSWORD=$SA_PASSWORD" -p 1433:1433 \ + -v diwan-mssql:/var/opt/mssql --restart unless-stopped mcr.microsoft.com/mssql/server:2022-latest + echo "waiting for SQL Server..."; until docker logs diwan-mssql 2>&1 | grep -q "ready for client connections"; do sleep 3; done +fi + +# 2. publish (from RMuseum/: its global.json pins the SDK newer Razor compilers can't build with) +stop +(cd RMuseum && dotnet publish RMuseum.csproj -c Release -o "$RUN/api" -p:EnableWindowsTargeting=true -v q \ + && dotnet publish ../DiwanRazor/DiwanRazor.csproj -c Release -o "$RUN/site" -p:EnableWindowsTargeting=true -v q) +git checkout -- RMuseum/RMuseum.xml 2>/dev/null || true # build regenerates this tracked doc file + +# 3. API (settings that RSecurityBackend only reads from appsettings.json are written into the published copy) +export ConnectionStrings__DefaultConnection="Server=localhost,1433;Database=diwan;User Id=sa;Password=$SA_PASSWORD;TrustServerCertificate=True;MultipleActiveResultSets=true" +export Security__Secret="local-dev-only-diwan-secret-key-0123456789abcdef" +export RSecurityBackend__ApplicationName=Diwan RSecurityBackend__FirstUserEmail=admin@diwan.local +export WebServiceUrl=http://localhost:5100 SignUp__Enabled=False +export PictureFileService__StoragePath=$RUN/museum PictureFileService__TrashStoragePath=$RUN/museum-trash +export AudioUploadService__TempUploadPath=$RUN/tempaudio AudioUploadService__LocalAudioRepositoryPath=$RUN/audio +export Diwan__SitemapLocation=$RUN/sitemap.xml PublicDataExport__LocalWorkingCopyPath=$RUN/export +(cd "$RUN/api" && python3 - <<'PY' +import json, os +f = 'appsettings.json'; d = json.load(open(f, encoding='utf-8-sig')) +d['ConnectionStrings']['DefaultConnection'] = os.environ['ConnectionStrings__DefaultConnection'] +d['Security']['Secret'] = os.environ['Security__Secret'] +d['RSecurityBackend']['ApplicationName'] = os.environ['RSecurityBackend__ApplicationName'] +d['RSecurityBackend']['FirstUserEmail'] = os.environ['RSecurityBackend__FirstUserEmail'] +json.dump(d, open(f, 'w', encoding='utf-8'), ensure_ascii=False, indent=2) +PY +ASPNETCORE_URLS=http://localhost:5100 nohup dotnet RMuseum.dll > "$RUN/logs/api.log" 2>&1 &) +until grep -q "Now listening" "$RUN/logs/api.log" 2>/dev/null; do sleep 2; done +curl -s -o /dev/null http://localhost:5100/api/diwan/poets # first request creates/migrates the database + +# 4. site +(cd "$RUN/site" && APIRoot=http://localhost:5100 GlobalAPIRoot=http://localhost:5100 SiteUrl=http://localhost:5200 \ + DataProtectionPersistPath=$RUN/keys TrackingScript="" SemanticSearchAPIRoot="" ASPNETCORE_URLS=http://localhost:5200 \ + nohup dotnet DiwanRazor.dll > "$RUN/logs/site.log" 2>&1 &) +until grep -q "Now listening" "$RUN/logs/site.log" 2>/dev/null; do sleep 2; done + +# 5. optional data import (background job; ~1 hour for all 11k poems; re-running only adds what's missing) +if [ "${1:-}" = import ]; then + if [ -f "$DATA/manifest.json" ]; then SRC="{\"useHttp\":false,\"location\":\"$(cd "$DATA" && pwd)\",\"poetId\":0}" + else SRC='{"useHttp":true,"location":"https://cdn.jsdelivr.net/gh/anas-rashid/diwan-data@main/","poetId":0}'; fi + TOKEN=$(curl -s -X POST http://localhost:5100/api/users/login -H 'Content-Type: application/json' \ + -d '{"username":"admin@diwan.local","password":"Test!123","clientAppName":"run-local","language":"ur-PK"}' \ + | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])") + curl -sf -X POST http://localhost:5100/api/diwan/publicdata/import -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' -d "$SRC" && echo "import started (progress: Admin -> long running jobs)" +fi + +echo "site: http://localhost:5200 API: http://localhost:5100/swagger logs: $RUN/logs"