diff --git a/.github/workflows/synology-release.yml b/.github/workflows/synology-release.yml new file mode 100644 index 0000000000..7151b22234 --- /dev/null +++ b/.github/workflows/synology-release.yml @@ -0,0 +1,64 @@ +name: Build Synology SPK Release + +on: + release: + types: [prereleased, released] + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 4.0.4 or latest)' + required: false + default: 'latest' + +jobs: + build-spk: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Get version + id: version + shell: bash + run: | + if [ "${{ github.event_name }}" = "release" ]; then + TAG="${GITHUB_REF#refs/tags/}" + echo "version=${TAG#v}" >> $GITHUB_OUTPUT + echo "tag=${TAG}" >> $GITHUB_OUTPUT + else + INPUT_VER="${{ github.event.inputs.version || 'latest' }}" + if [ "$INPUT_VER" = "latest" ]; then + echo "version=latest" >> $GITHUB_OUTPUT + echo "tag=latest" >> $GITHUB_OUTPUT + else + echo "version=${INPUT_VER#v}" >> $GITHUB_OUTPUT + echo "tag=v${INPUT_VER#v}" >> $GITHUB_OUTPUT + fi + fi + + - name: Build Synology SPK + run: | + make package-synology VERSION=${{ steps.version.outputs.version }} + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: exelearning-${{ steps.version.outputs.version }}-spk + path: release/exelearning-*.spk + + - name: Upload to GitHub Release + if: github.event_name == 'release' + run: | + gh release upload ${{ steps.version.outputs.tag }} \ + release/exelearning-${{ steps.version.outputs.version }}.spk \ + --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index a2e3a113b5..c4f501022b 100644 --- a/Makefile +++ b/Makefile @@ -829,6 +829,14 @@ package-windows-local-sign: fail-on-windows check-release-env eb-inject-config ## --------- END WINDOWS LOCAL SIGN --------- +# Build Synology SPK package +# Usage: make package-synology VERSION=4.0.4 +.PHONY: package-synology +package-synology: + @$(if $(VERSION),,$(error VERSION is required. Usage: make package-synology VERSION=x.y.z [DOCKER_TAG=latest])) + @DOCKER_TAG="$(DOCKER_TAG)" bash packaging/synology/build-spk.sh $(VERSION) + + # ============================================================================= # UTILITIES @@ -972,5 +980,6 @@ help: @echo "Packaging:" @echo " make package VERSION=1.0.0 Build release" @echo " make package VERSION=1.0.0 PUBLISH=always Build & publish to GitHub" + @echo " make package-synology VERSION=1.0.0 Build Synology SPK package" @echo " make package-windows-local-sign VERSION=1.0.0 CERT_THUMBPRINT=xxx" @echo " Build signed Windows release" diff --git a/doc/deploy/synology.md b/doc/deploy/synology.md new file mode 100644 index 0000000000..91ce5868c2 --- /dev/null +++ b/doc/deploy/synology.md @@ -0,0 +1,134 @@ +# Despliegue en Synology NAS (SPK / Container Manager) + +eXeLearning se puede instalar y ejecutar en servidores NAS de Synology mediante un paquete oficial **SPK (Synology Package)** ligero que aprovecha el recurso nativo `docker-project` de **Container Manager** (DSM 7.2.1+). + +--- + +## 1. Arquitectura del Paquete + +El SPK actúa como un envoltorio nativo que se integra con DSM: + +```mermaid +flowchart TD + DSM["Synology DSM (Package Center)"] --> SPK["eXeLearning SPK"] + SPK --> CM["Container Manager (docker-project)"] + CM --> IMG["Docker Image: ghcr.io/exelearning/exelearning:tag"] + IMG --> VOL["Volumen NAS: /volume1/docker/exelearning"] + IMG --> SSO["Synology SSO Server (OIDC)"] +``` + +- **Tamaño ligero:** El SPK pesa ~100 KB (solo metadatos, asistentes de interfaz y scripts de ciclo de vida). +- **Imagen oficial multiarquitectura:** Descarga automáticamente la imagen oficial optimizada para `linux/amd64` (Intel/AMD) o `linux/arm64` (ARMv8). +- **Persistencia garantizada:** Los datos, base de datos SQLite y assets se almacenan en una carpeta compartida del NAS (`/volume1/docker/exelearning` por defecto). + +--- + +## 2. Requisitos Previos + +- **Sistema operativo:** Synology DSM **7.2.1-69057** o superior. +- **Paquetes requeridos:** **Container Manager** (versión 1432 o superior), disponible de forma gratuita en el Package Center. +- **Arquitectura de CPU:** Modelos Synology con procesador **x86_64** (Intel/AMD) o **ARM64** (Realtek RTD1619b, etc. compatibles con Docker). + +--- + +## 3. Instalación + +### Opción A: Instalación manual del archivo `.spk` + +1. Descarga el archivo `exelearning-.spk` desde los [Releases oficiales de GitHub](https://github.com/exelearning/exelearning/releases). +2. Entra en el panel de control de tu Synology DSM y abre **Package Center** (Centro de paquetes). +3. Haz clic en el botón superior **Instalación manual** (Manual Install) y selecciona el archivo `.spk`. +4. El asistente de instalación solicitará: + - **Puerto web:** Puerto local del NAS para eXeLearning (por defecto `8080`). + - **Ruta de datos:** Carpeta en el NAS donde se guardarán proyectos y configuraciones (por defecto `/volume1/docker/exelearning`). + - **Método de autenticación:** Modo inicial (`password,guest`, `password`, `openid,password`, o `none`). +5. Confirma la instalación. Container Manager descargará la imagen y levantará el servicio automáticamente. + +--- + +### Opción B: Repositorio de paquetes (Package Center) + +1. En **Package Center**, pulsa en **Configuración** -> pestaña **Orígenes de paquetes** (Package Sources). +2. Añade un nuevo origen: + - **Nombre:** `eXeLearning` + - **Ubicación:** `https://synology.exelearning.net/` (o la URL de GitHub Pages configurada) +3. Ve a la sección **Comunidad** en el Package Center y haz clic en **Instalar** en eXeLearning. + +--- + +## 4. Federación de Login con Synology SSO (OpenID Connect) + +Es posible permitir que los usuarios de tu Synology NAS inicien sesión en eXeLearning utilizando sus credenciales de DSM (locales o de Directorio LDAP/Active Directory). + +```mermaid +sequenceDiagram + autonumber + actor Usuario + participant eXe as eXeLearning (Web) + participant SSO as Synology SSO Server + + Usuario->>eXe: Clic en "Iniciar sesión con Synology" + eXe->>SSO: Redirige a https://nas:5001/sso/ + SSO-->>Usuario: Solicita credenciales de DSM + Usuario->>SSO: Autenticación exitosa + SSO-->>eXe: Retorna Authorization Code + eXe->>SSO: Intercambia token y obtiene perfil (UserInfo) + eXe->>eXe: Crea/enlaza el usuario + eXe-->>Usuario: Sesión iniciada +``` + +### Paso 1: Instalar Synology SSO Server +1. En el **Package Center** de DSM, busca e instala **SSO Server**. +2. Abre **SSO Server** y en **Servicio** activa la casilla **Habilitar el servicio OIDC**. + +### Paso 2: Registrar la aplicación eXeLearning +1. Dentro de **SSO Server**, dirígete a **Lista de aplicaciones** -> **Añadir**. +2. Completa los datos: + - **Nombre de la aplicación:** `eXeLearning` + - **URI de redirección:** `https://:/login/openid/callback` + *(ejemplo: `https://nas.local:8080/login/openid/callback` o la URL configurada en el proxy inverso)* +3. Guarda y copia el **ID de cliente** (App ID) y el **Secreto de cliente** (App Secret). + +### Paso 3: Configurar eXeLearning +Edita el archivo `.env` ubicado en la carpeta de instalación del paquete (`/var/packages/exelearning/target/.env` o en el proyecto de Container Manager): + +```env +APP_AUTH_METHODS=openid,password +AUTH_CREATE_USERS=true + +# Configuración SSO de Synology +OIDC_ISSUER=https://:5001/sso +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= +OIDC_SCOPE="openid email profile" +``` + +Reinicia el contenedor desde Container Manager. Ahora aparecerá el botón de inicio de sesión OpenID / Synology en la pantalla de bienvenida. + +--- + +## 5. Configurar HTTPS y Dominio con el Proxy Inverso de DSM + +Para acceder de forma segura a través de `https://exelearning.tu-dominio.com`: + +1. Ve a **Panel de control** -> **Portal de inicio de sesión** -> pestaña **Avanzado**. +2. Haz clic en **Proxy inverso** -> **Crear**: + - **Nombre descriptivo:** `eXeLearning` + - **Origen:** + - Protocolo: `HTTPS` + - Nombre de host: `exelearning.tu-dominio.com` + - Puerto: `443` + - Activar HSTS y HTTP/2 + - **Destino:** + - Protocolo: `HTTP` + - Nombre de host: `localhost` + - Puerto: `8080` (o el puerto configurado en el SPK) +3. En la pestaña **Encabezado personalizado**, pulsa en **Crear** -> **WebSocket** (para asegurar soporte de colaboración en tiempo real). +4. Ve a **Panel de control** -> **Seguridad** -> **Certificado** y asigna tu certificado SSL (Let's Encrypt) a la regla de proxy inverso creada. + +--- + +## 6. Copias de Seguridad y Mantenimiento + +- **Base de datos y archivos:** Todo el estado se almacena en la ruta de datos configurada (`/volume1/docker/exelearning`). Puedes incluir este directorio en las tareas de **Hyper Backup** de Synology. +- **Actualizaciones:** Al actualizar el paquete desde el Package Center o reinstalar un `.spk` más reciente, la configuración y los datos del volumen persistente se preservan intactos. diff --git a/mkdocs.yml b/mkdocs.yml index 246a74226a..6daa946380 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,7 @@ nav: - Deployment: deployment.md - High Availability: high-availability.md - "Deploy: Sample Configs": deploy/README.md + - "Deploy: Synology NAS": deploy/synology.md - Architecture: - Overview: architecture.md - Conventions: conventions.md diff --git a/package.json b/package.json index 2d63ab8bc1..5726367c17 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "export-scorm2004": "bun run dist/cli.js elp:export --format=scorm2004", "export-ims": "bun run dist/cli.js elp:export --format=ims", "export-epub3": "bun run dist/cli.js elp:export --format=epub3", - "test:unit": "bun test ./src ./test/helpers ./scripts ./app --coverage", - "test:unit:ci": "bun test ./src ./test/helpers ./scripts ./app --coverage --coverage-reporter=lcov --coverage-dir=coverage/bun --reporter=junit --reporter-outfile=coverage/bun/junit.xml", + "test:unit": "bun test ./src ./test/helpers ./scripts ./app ./packaging --coverage", + "test:unit:ci": "bun test ./src ./test/helpers ./scripts ./app ./packaging --coverage --coverage-reporter=lcov --coverage-dir=coverage/bun --reporter=junit --reporter-outfile=coverage/bun/junit.xml", "test:integration": "bun test ./test/integration", "test:frontend": "vitest run --config vitest.config.mts --coverage --reporter=default --reporter=junit --outputFile=coverage/vitest/junit.xml", "test:frontend:ui": "vitest --ui --config vitest.config.mts", diff --git a/packaging/synology/INFO.template b/packaging/synology/INFO.template new file mode 100644 index 0000000000..ba0c0105a5 --- /dev/null +++ b/packaging/synology/INFO.template @@ -0,0 +1,19 @@ +package="exelearning" +name="exelearning" +version="{{VERSION}}" +displayname="eXeLearning" +maintainer="INTEF" +maintainer_url="https://exelearning.net" +distributor="INTEF" +distributor_url="https://exelearning.net" +support_url="https://exelearning.net" +description="eXeLearning authoring tool to create interactive educational contents and export to SCORM, IMS and HTML5." +os_min_ver="7.0-40000" +install_type="system" +arch="apollolake x86_64 noarch armv8 aarch64 geminilake broadwell v1000 rtd1296 rtd1619b" +startable="yes" +dsmuidir="ui" +dsmappname="SYNO.SDS.eXeLearning" +adminport="8085" +adminurl="/" +helpurl="https://exelearning.net" diff --git a/packaging/synology/PACKAGE_ICON.PNG b/packaging/synology/PACKAGE_ICON.PNG new file mode 100644 index 0000000000..10e4b5f4d4 Binary files /dev/null and b/packaging/synology/PACKAGE_ICON.PNG differ diff --git a/packaging/synology/PACKAGE_ICON_256.PNG b/packaging/synology/PACKAGE_ICON_256.PNG new file mode 100644 index 0000000000..5203a4165b Binary files /dev/null and b/packaging/synology/PACKAGE_ICON_256.PNG differ diff --git a/packaging/synology/WIZARD_UIFILES/install_uifile b/packaging/synology/WIZARD_UIFILES/install_uifile new file mode 100644 index 0000000000..45cd0cf24b --- /dev/null +++ b/packaging/synology/WIZARD_UIFILES/install_uifile @@ -0,0 +1,60 @@ +[ + { + "step_title": "Network and Storage Settings", + "items": [ + { + "type": "textfield", + "desc": "Web port to access eXeLearning:", + "subitems": [ + { + "key": "wizard_port", + "desc": "Port", + "defaultValue": "8080", + "validator": { + "regex": { + "expr": "^[0-9]+$", + "errorText": "Port must be an integer between 1024 and 65535" + } + } + } + ] + }, + { + "type": "textfield", + "desc": "Storage folder on NAS for projects and data:", + "subitems": [ + { + "key": "wizard_data_path", + "desc": "Data path", + "defaultValue": "/volume1/docker/exelearning" + } + ] + } + ] + }, + { + "step_title": "Authentication Mode", + "items": [ + { + "type": "combobox", + "desc": "Select default authentication mode:", + "subitems": [ + { + "key": "wizard_auth_methods", + "defaultValue": "password,guest", + "displayField": "display", + "valueField": "value", + "mode": "local", + "editable": false, + "store": [ + ["password,guest", "Local password and Guest access"], + ["password", "Local password only"], + ["openid,password", "OpenID Connect (SSO) and Local password"], + ["none", "Open / Offline mode (No login)"] + ] + } + ] + } + ] + } +] diff --git a/packaging/synology/WIZARD_UIFILES/upgrade_uifile b/packaging/synology/WIZARD_UIFILES/upgrade_uifile new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packaging/synology/WIZARD_UIFILES/upgrade_uifile @@ -0,0 +1 @@ +[] diff --git a/packaging/synology/build-spk.sh b/packaging/synology/build-spk.sh new file mode 100755 index 0000000000..42a2dcf24f --- /dev/null +++ b/packaging/synology/build-spk.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Ensure macOS bsdtar does not include ._* metadata files or extended attributes +export COPYFILE_DISABLE=1 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +VERSION_INPUT="${1:-latest}" + +# Strip leading 'v' for SPK metadata version +CLEAN_VERSION="${VERSION_INPUT#v}" + +if [ "$CLEAN_VERSION" = "latest" ] || [ -z "$CLEAN_VERSION" ]; then + # Extract version from package.json or fallback + PKG_VER="$(node -p "try { require('${PROJECT_ROOT}/package.json').version } catch(e) { '4.0.4' }" 2>/dev/null || echo "4.0.4")" + BASE_VERSION="${PKG_VER#v}" + SPK_VERSION="${BASE_VERSION}-0001" + DOCKER_TAG="latest" +elif [[ "$CLEAN_VERSION" =~ - ]]; then + BASE_VERSION="$CLEAN_VERSION" + SPK_VERSION="$BASE_VERSION" + DOCKER_TAG="${DOCKER_TAG:-$VERSION_INPUT}" +else + BASE_VERSION="$CLEAN_VERSION" + SPK_VERSION="${BASE_VERSION}-0001" + DOCKER_TAG="${DOCKER_TAG:-$VERSION_INPUT}" +fi + +OUTPUT_DIR="${2:-${PROJECT_ROOT}/release}" +mkdir -p "$OUTPUT_DIR" + +STAGE_DIR="$(mktemp -d -t exelearning-spk-XXXXXX)" +cleanup() { + rm -rf "$STAGE_DIR" +} +trap cleanup EXIT + +echo "==> Building Synology SPK for eXeLearning ${SPK_VERSION} (Docker image: exelearning/exelearning:${DOCKER_TAG})..." + +# 1. Prepare stage directories +mkdir -p "${STAGE_DIR}/package/docker" +mkdir -p "${STAGE_DIR}/package/ui/images" +mkdir -p "${STAGE_DIR}/conf" +mkdir -p "${STAGE_DIR}/scripts" + +# 2. Template and copy INFO +sed "s/{{VERSION}}/${SPK_VERSION}/g" "${SCRIPT_DIR}/INFO.template" > "${STAGE_DIR}/INFO" + +# 3. Copy icons +if [ -f "${SCRIPT_DIR}/PACKAGE_ICON.PNG" ]; then + cp "${SCRIPT_DIR}/PACKAGE_ICON.PNG" "${STAGE_DIR}/PACKAGE_ICON.PNG" +fi +if [ -f "${SCRIPT_DIR}/PACKAGE_ICON_256.PNG" ]; then + cp "${SCRIPT_DIR}/PACKAGE_ICON_256.PNG" "${STAGE_DIR}/PACKAGE_ICON_256.PNG" +fi + +# 4. Copy conf files +cp "${SCRIPT_DIR}/conf/privilege" "${STAGE_DIR}/conf/privilege" +cp "${SCRIPT_DIR}/conf/resource" "${STAGE_DIR}/conf/resource" + +# 5. Copy and set executable permissions for scripts +if [ -d "${SCRIPT_DIR}/scripts" ]; then + cp -r "${SCRIPT_DIR}/scripts/"* "${STAGE_DIR}/scripts/" 2>/dev/null || true + chmod 755 "${STAGE_DIR}/scripts/"* 2>/dev/null || true +fi + +# 6. Prepare package payload (docker/docker-compose.yml and DSM UI integration) +sed "s/\${VERSION}/${DOCKER_TAG}/g" "${SCRIPT_DIR}/docker/docker-compose.yml.tpl" > "${STAGE_DIR}/package/docker/docker-compose.yml" + +if [ -f "${SCRIPT_DIR}/ui/config" ]; then + cp "${SCRIPT_DIR}/ui/config" "${STAGE_DIR}/package/ui/config" +fi +if [ -f "${SCRIPT_DIR}/PACKAGE_ICON.PNG" ]; then + cp "${SCRIPT_DIR}/PACKAGE_ICON.PNG" "${STAGE_DIR}/package/ui/images/icon_64.png" + cp "${SCRIPT_DIR}/PACKAGE_ICON.PNG" "${STAGE_DIR}/package/ui/images/icon.png" +fi +if [ -f "${SCRIPT_DIR}/PACKAGE_ICON_256.PNG" ]; then + cp "${SCRIPT_DIR}/PACKAGE_ICON_256.PNG" "${STAGE_DIR}/package/ui/images/icon_256.png" +fi + +# Check individual tar support for cross-platform Linux (GNU tar) and macOS (bsdtar) +TAR_EXTRA_OPTS=() +if tar --help 2>&1 | grep -q -- '--no-xattrs'; then + TAR_EXTRA_OPTS+=( "--no-xattrs" ) +fi +if tar --help 2>&1 | grep -q -- '--no-mac-metadata'; then + TAR_EXTRA_OPTS+=( "--no-mac-metadata" ) +fi + +# Create package.tgz in standard format +( + cd "${STAGE_DIR}/package" + TAR_CMD=( "tar" "-czf" "${STAGE_DIR}/package.tgz" ) + if [ ${#TAR_EXTRA_OPTS[@]} -gt 0 ]; then + TAR_CMD+=( "${TAR_EXTRA_OPTS[@]}" ) + fi + "${TAR_CMD[@]}" * +) +rm -rf "${STAGE_DIR}/package" + +# 7. Create final SPK archive (tar without compression, ustar format, INFO strictly first) +SPK_FILENAME="exelearning-${BASE_VERSION}.spk" +SPK_PATH="${OUTPUT_DIR}/${SPK_FILENAME}" + +TAR_ITEMS=( "INFO" ) + +if [ -f "${STAGE_DIR}/PACKAGE_ICON.PNG" ]; then + TAR_ITEMS+=( "PACKAGE_ICON.PNG" ) +fi +if [ -f "${STAGE_DIR}/PACKAGE_ICON_256.PNG" ]; then + TAR_ITEMS+=( "PACKAGE_ICON_256.PNG" ) +fi +if [ -d "${STAGE_DIR}/conf" ]; then + TAR_ITEMS+=( "conf" ) +fi +if [ -d "${STAGE_DIR}/scripts" ]; then + TAR_ITEMS+=( "scripts" ) +fi +if [ -f "${STAGE_DIR}/package.tgz" ]; then + TAR_ITEMS+=( "package.tgz" ) +fi + +( + cd "${STAGE_DIR}" + SPK_CMD=( "tar" "-cf" "${SPK_PATH}" ) + if tar --help 2>&1 | grep -q -- '--format'; then + SPK_CMD+=( "--format" "ustar" ) + fi + if [ ${#TAR_EXTRA_OPTS[@]} -gt 0 ]; then + SPK_CMD+=( "${TAR_EXTRA_OPTS[@]}" ) + fi + "${SPK_CMD[@]}" "${TAR_ITEMS[@]}" +) + +echo "==> Synology SPK created successfully: ${SPK_PATH}" diff --git a/packaging/synology/build-spk.spec.ts b/packaging/synology/build-spk.spec.ts new file mode 100644 index 0000000000..acf5ded653 --- /dev/null +++ b/packaging/synology/build-spk.spec.ts @@ -0,0 +1,112 @@ +import { describe, it, expect, afterAll } from 'bun:test'; +import { execSync, spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +const projectRoot = path.resolve(__dirname, '../..'); +const buildScript = path.join(projectRoot, 'packaging/synology/build-spk.sh'); +const testOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'synology-spk-test-')); + +afterAll(() => { + fs.rmSync(testOutputDir, { recursive: true, force: true }); +}); + +describe('packaging/synology/build-spk.sh', () => { + it('should default to latest docker tag and package version when version is omitted or latest', () => { + const result = spawnSync('bash', [buildScript, 'latest', testOutputDir], { + cwd: projectRoot, + encoding: 'utf8', + }); + expect(result.status).toBe(0); + + const spkFiles = fs.readdirSync(testOutputDir).filter((f) => f.endsWith('.spk')); + expect(spkFiles.length).toBeGreaterThan(0); + const spkFile = path.join(testOutputDir, spkFiles[0]); + + const composeContent = execSync( + `tar -xOf "${spkFile}" package.tgz | tar -xzO docker/docker-compose.yml`, + { encoding: 'utf8' } + ); + expect(composeContent).toContain('exelearning/exelearning:latest'); + }); + + it('should generate a valid SPK archive with version substituted', () => { + const version = '4.0.4'; + const result = spawnSync('bash', [buildScript, version, testOutputDir], { + cwd: projectRoot, + encoding: 'utf8', + }); + + expect(result.status).toBe(0); + const spkFile = path.join(testOutputDir, `exelearning-${version}.spk`); + expect(fs.existsSync(spkFile)).toBe(true); + + // Verify tar table of contents and ensure INFO is the first entry + const tocOutput = execSync(`tar -tf "${spkFile}"`, { encoding: 'utf8' }); + const entries = tocOutput.trim().split('\n'); + expect(entries[0]).toBe('INFO'); + expect(tocOutput).toContain('PACKAGE_ICON.PNG'); + expect(tocOutput).toContain('PACKAGE_ICON_256.PNG'); + expect(tocOutput).toContain('conf/privilege'); + expect(tocOutput).toContain('conf/resource'); + expect(tocOutput).toContain('package.tgz'); + expect(tocOutput).toContain('scripts/start-stop-status'); + expect(tocOutput).toContain('scripts/preinst'); + expect(tocOutput).toContain('scripts/postinst'); + + // Verify INFO file contents inside the SPK + const infoContent = execSync(`tar -xOf "${spkFile}" INFO`, { encoding: 'utf8' }); + expect(infoContent).toContain('package="exelearning"'); + expect(infoContent).toContain('version="4.0.4-0001"'); + expect(infoContent).toContain('os_min_ver="7.0-40000"'); + expect(infoContent).toContain('install_type="system"'); + expect(infoContent).toContain('dsmuidir="ui"'); + expect(infoContent).toContain('dsmappname="SYNO.SDS.eXeLearning"'); + expect(infoContent).toContain('adminport="8085"'); + + // Verify docker/docker-compose.yml contents inside package.tgz inside the SPK + const composeContent = execSync( + `tar -xOf "${spkFile}" package.tgz | tar -xzO docker/docker-compose.yml`, + { encoding: 'utf8' } + ); + expect(composeContent).toContain(`exelearning/exelearning:${version}`); + expect(composeContent).toContain('exelearning_data:/mnt/data'); + + // Verify ui/config inside package.tgz + const uiConfigContent = execSync( + `tar -xOf "${spkFile}" package.tgz | tar -xzO ui/config`, + { encoding: 'utf8' } + ); + expect(uiConfigContent).toContain('"SYNO.SDS.eXeLearning"'); + expect(uiConfigContent).toContain('"port": "8085"'); + + // Verify resource file uses projects schema + const resourceContent = execSync(`tar -xOf "${spkFile}" conf/resource`, { encoding: 'utf8' }); + expect(resourceContent).toContain('"docker-project"'); + expect(resourceContent).toContain('"projects"'); + expect(resourceContent).toContain('"path": "docker"'); + }); + + it('should handle version with leading v prefix (e.g. v4.0.2)', () => { + const versionInput = 'v4.0.2'; + const expectedVersion = '4.0.2'; + const result = spawnSync('bash', [buildScript, versionInput, testOutputDir], { + cwd: projectRoot, + encoding: 'utf8', + }); + + expect(result.status).toBe(0); + const spkFile = path.join(testOutputDir, `exelearning-${expectedVersion}.spk`); + expect(fs.existsSync(spkFile)).toBe(true); + + const infoContent = execSync(`tar -xOf "${spkFile}" INFO`, { encoding: 'utf8' }); + expect(infoContent).toContain('version="4.0.2-0001"'); + + const composeContent = execSync( + `tar -xOf "${spkFile}" package.tgz | tar -xzO docker/docker-compose.yml`, + { encoding: 'utf8' } + ); + expect(composeContent).toContain('exelearning/exelearning:v4.0.2'); + }); +}); diff --git a/packaging/synology/conf/privilege b/packaging/synology/conf/privilege new file mode 100644 index 0000000000..dc2d99d1e6 --- /dev/null +++ b/packaging/synology/conf/privilege @@ -0,0 +1,5 @@ +{ + "defaults": { + "run-as": "package" + } +} diff --git a/packaging/synology/conf/resource b/packaging/synology/conf/resource new file mode 100644 index 0000000000..fedb3da97f --- /dev/null +++ b/packaging/synology/conf/resource @@ -0,0 +1,10 @@ +{ + "docker-project": { + "projects": [ + { + "name": "exelearning", + "path": "docker" + } + ] + } +} diff --git a/packaging/synology/docker/docker-compose.yml.tpl b/packaging/synology/docker/docker-compose.yml.tpl new file mode 100644 index 0000000000..4add7ec005 --- /dev/null +++ b/packaging/synology/docker/docker-compose.yml.tpl @@ -0,0 +1,31 @@ +services: + exelearning: + image: exelearning/exelearning:${VERSION} + container_name: exelearning + restart: unless-stopped + ports: + - "${PORT:-8085}:8080" + environment: + - APP_ENV=prod + - NODE_ENV=production + - FILES_DIR=/mnt/data/ + - DB_PATH=/mnt/data/exelearning.db + - DB_DRIVER=pdo_sqlite + - APP_AUTH_METHODS=${APP_AUTH_METHODS:-password,guest} + - AUTH_CREATE_USERS=${AUTH_CREATE_USERS:-true} + - OIDC_ISSUER=${OIDC_ISSUER:-} + - OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-} + - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET:-} + - OIDC_SCOPE=${OIDC_SCOPE:-openid email profile} + volumes: + - exelearning_data:/mnt/data + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthcheck || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + exelearning_data: + name: exelearning_data diff --git a/packaging/synology/scripts/postinst b/packaging/synology/scripts/postinst new file mode 100755 index 0000000000..319e71e871 --- /dev/null +++ b/packaging/synology/scripts/postinst @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +ENV_FILE="${SYNOPKG_PKGDEST}/docker/.env" +if [ ! -f "$ENV_FILE" ]; then +cat < "$ENV_FILE" +PORT=8085 +APP_AUTH_METHODS=password,guest +AUTH_CREATE_USERS=true +EOF +fi + +exit 0 diff --git a/packaging/synology/scripts/postun b/packaging/synology/scripts/postun new file mode 100755 index 0000000000..11f5e61b08 --- /dev/null +++ b/packaging/synology/scripts/postun @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +# Data directory is preserved on uninstallation to avoid accidental data loss. +exit 0 diff --git a/packaging/synology/scripts/postupgrade b/packaging/synology/scripts/postupgrade new file mode 100755 index 0000000000..319e71e871 --- /dev/null +++ b/packaging/synology/scripts/postupgrade @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +ENV_FILE="${SYNOPKG_PKGDEST}/docker/.env" +if [ ! -f "$ENV_FILE" ]; then +cat < "$ENV_FILE" +PORT=8085 +APP_AUTH_METHODS=password,guest +AUTH_CREATE_USERS=true +EOF +fi + +exit 0 diff --git a/packaging/synology/scripts/preinst b/packaging/synology/scripts/preinst new file mode 100755 index 0000000000..541a41f537 --- /dev/null +++ b/packaging/synology/scripts/preinst @@ -0,0 +1,4 @@ +#!/bin/sh +set -e + +exit 0 diff --git a/packaging/synology/scripts/start-stop-status b/packaging/synology/scripts/start-stop-status new file mode 100755 index 0000000000..f4929f6b4e --- /dev/null +++ b/packaging/synology/scripts/start-stop-status @@ -0,0 +1,17 @@ +#!/bin/sh + +case "$1" in + start) + exit 0 + ;; + stop) + exit 0 + ;; + status) + exit 0 + ;; + *) + echo "Usage: $0 {start|stop|status}" + exit 1 + ;; +esac diff --git a/packaging/synology/ui/config b/packaging/synology/ui/config new file mode 100644 index 0000000000..d04220024c --- /dev/null +++ b/packaging/synology/ui/config @@ -0,0 +1,14 @@ +{ + ".url": { + "SYNO.SDS.eXeLearning": { + "title": "eXeLearning", + "desc": "eXeLearning authoring tool", + "icon": "images/icon_64.png", + "type": "url", + "protocol": "http", + "port": "8085", + "url": "/", + "allUsers": true + } + } +}