From 55f201da40550691b3ba8f07d164b550f29969a9 Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 10:39:02 +0200 Subject: [PATCH] Add admin deployment sync --- Dockerfile | 4 ++-- README.md | 4 ++++ admin.html | 7 +++++++ assets/js/admin.js | 24 ++++++++++++++++++++++++ backend/server.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 ++ 6 files changed, 84 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c65a3c5..eb2f3df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,8 @@ RUN bundle exec jekyll build # === Serve-Phase: node.js backend + ImageMagick === FROM node:20-alpine -# ImageMagick fuer Thumbnails, Build-Tools fuer sqlite3 native bindings -RUN apk add --no-cache imagemagick python3 make g++ build-base sqlite-dev +# ImageMagick fuer Thumbnails, Docker CLI fuer Admin-Sync, Build-Tools fuer sqlite3 native bindings +RUN apk add --no-cache imagemagick docker-cli python3 make g++ build-base sqlite-dev WORKDIR /app COPY package*.json ./ diff --git a/README.md b/README.md index 59c2591..b6d726b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ services: - INITIAL_ADMIN_PASSWORD=Start123 - FULLS_DIR=/app/public/images/fulls - THUMBS_DIR=/app/public/images/thumbs + - SYNC_IMAGE=ghcr.io/kroonk/photography:latest + - SYNC_CONTAINER=photography-website volumes: - /path/to/nextcloud/data:/app/public/images/fulls:ro - thumbs:/app/public/images/thumbs @@ -44,6 +46,8 @@ Change the initial admin password after the first login. If you prefer not to st node -e "const bcrypt = require('bcryptjs'); console.log(bcrypt.hashSync('your-admin-password', 10));" ``` +The admin dashboard includes a sync button that pulls `SYNC_IMAGE` and restarts `SYNC_CONTAINER`. This requires the Docker socket mount shown above. + ## Credits & License - Originally based on a Jekyll template. - UI Design by [AJ / HTML5 UP](https://html5up.net). diff --git a/admin.html b/admin.html index 1f7835d..9ce97d4 100644 --- a/admin.html +++ b/admin.html @@ -74,6 +74,13 @@ +
+

Website aktualisieren

+

Laedt das neueste Docker-Image und startet den Website-Container neu.

+ + +
+

Eigenes Passwort aendern

diff --git a/assets/js/admin.js b/assets/js/admin.js index f1570c0..f3516c5 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -172,6 +172,30 @@ $(document).ready(function() { }); }); + // ==================== WEBSITE SYNC ==================== + + $('#sync-site').click(function() { + if (!confirm('Neueste Version laden und Website neu starten?')) return; + + var $button = $(this); + var $status = $('#sync-status'); + $button.prop('disabled', true); + $status.text('Sync laeuft...'); + + $.post('/api/admin/sync') + .done(function(res) { + $status.text(res.message || 'Sync gestartet. Website startet neu...'); + window.setTimeout(function() { + window.location.reload(); + }, 12000); + }) + .fail(function(err) { + var msg = err.responseJSON ? err.responseJSON.error : 'Sync fehlgeschlagen'; + $status.text(msg); + $button.prop('disabled', false); + }); + }); + // ==================== EIGENES PASSWORT ==================== $('#admin-pw-form').submit(function(e) { diff --git a/backend/server.js b/backend/server.js index 55f6323..1d24d50 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const fs = require('fs'); const path = require('path'); +const { execFile } = require('child_process'); const db = require('./database'); const app = express(); @@ -18,7 +19,10 @@ const DEV_FULLS_DIR = path.join(__dirname, '../images/fulls'); const DEV_THUMBS_DIR = path.join(__dirname, '../images/thumbs'); const FULLS_DIR = process.env.FULLS_DIR || (fs.existsSync(DEFAULT_FULLS_DIR) ? DEFAULT_FULLS_DIR : DEV_FULLS_DIR); const THUMBS_DIR = process.env.THUMBS_DIR || (fs.existsSync(DEFAULT_THUMBS_DIR) ? DEFAULT_THUMBS_DIR : DEV_THUMBS_DIR); +const SYNC_IMAGE = process.env.SYNC_IMAGE || 'ghcr.io/kroonk/photography:latest'; +const SYNC_CONTAINER = process.env.SYNC_CONTAINER || 'photography-website'; const IMAGE_RE = /\.(jpg|jpeg|png|gif|webp)$/i; +let syncInProgress = false; console.log('Using full-size images at', FULLS_DIR); console.log('Using thumbnails at', THUMBS_DIR); @@ -134,6 +138,12 @@ function serveProtectedImage(req, res) { }); } +function runDocker(args, callback) { + execFile('docker', args, { timeout: 5 * 60 * 1000 }, (err, stdout, stderr) => { + callback(err, `${stdout || ''}${stderr || ''}`.trim()); + }); +} + // ==================== AUTH API ==================== app.post('/api/auth/login', (req, res) => { @@ -309,6 +319,41 @@ app.post('/api/admin/assign', requireAdmin, (req, res) => { }); }); +app.post('/api/admin/sync', requireAdmin, (req, res) => { + if (syncInProgress) { + return res.status(409).json({ error: 'Sync laeuft bereits' }); + } + + syncInProgress = true; + runDocker(['pull', SYNC_IMAGE], (pullErr, pullOutput) => { + syncInProgress = false; + + if (pullErr) { + console.error('Sync pull failed:', pullOutput || pullErr.message); + return res.status(500).json({ + error: 'Docker pull fehlgeschlagen', + details: pullOutput || pullErr.message + }); + } + + res.json({ + success: true, + message: 'Neues Image geladen. Container startet neu.', + output: pullOutput + }); + + setTimeout(() => { + runDocker(['restart', SYNC_CONTAINER], (restartErr, restartOutput) => { + if (restartErr) { + console.error('Sync restart failed:', restartOutput || restartErr.message); + return; + } + console.log('Sync restart complete:', restartOutput); + }); + }, 500); + }); +}); + // ==================== STATIC FILES ==================== app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage); diff --git a/docker-compose.yml b/docker-compose.yml index 6597104..ce309ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: - INITIAL_ADMIN_PASSWORD=Start123 - FULLS_DIR=/app/public/images/fulls - THUMBS_DIR=/app/public/images/thumbs + - SYNC_IMAGE=ghcr.io/kroonk/photography:latest + - SYNC_CONTAINER=photography-website volumes: # Nextcloud-Ordner mit Originalbildern (read-only) - /volume1/nextcloud/data/Tom/files/Websitebilder:/app/public/images/fulls:ro