Add admin deployment sync
Some checks failed
Build & Push Docker Image / build-and-push (push) Failing after 14s
Some checks failed
Build & Push Docker Image / build-and-push (push) Failing after 14s
This commit is contained in:
@@ -15,8 +15,8 @@ RUN bundle exec jekyll build
|
|||||||
# === Serve-Phase: node.js backend + ImageMagick ===
|
# === Serve-Phase: node.js backend + ImageMagick ===
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
# ImageMagick fuer Thumbnails, Build-Tools fuer sqlite3 native bindings
|
# ImageMagick fuer Thumbnails, Docker CLI fuer Admin-Sync, Build-Tools fuer sqlite3 native bindings
|
||||||
RUN apk add --no-cache imagemagick python3 make g++ build-base sqlite-dev
|
RUN apk add --no-cache imagemagick docker-cli python3 make g++ build-base sqlite-dev
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ services:
|
|||||||
- INITIAL_ADMIN_PASSWORD=Start123
|
- INITIAL_ADMIN_PASSWORD=Start123
|
||||||
- FULLS_DIR=/app/public/images/fulls
|
- FULLS_DIR=/app/public/images/fulls
|
||||||
- THUMBS_DIR=/app/public/images/thumbs
|
- THUMBS_DIR=/app/public/images/thumbs
|
||||||
|
- SYNC_IMAGE=ghcr.io/kroonk/photography:latest
|
||||||
|
- SYNC_CONTAINER=photography-website
|
||||||
volumes:
|
volumes:
|
||||||
- /path/to/nextcloud/data:/app/public/images/fulls:ro
|
- /path/to/nextcloud/data:/app/public/images/fulls:ro
|
||||||
- thumbs:/app/public/images/thumbs
|
- 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));"
|
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
|
## Credits & License
|
||||||
- Originally based on a Jekyll template.
|
- Originally based on a Jekyll template.
|
||||||
- UI Design by [AJ / HTML5 UP](https://html5up.net).
|
- UI Design by [AJ / HTML5 UP](https://html5up.net).
|
||||||
|
|||||||
@@ -74,6 +74,13 @@
|
|||||||
<span id="assign-status" style="margin-left:10px;"></span>
|
<span id="assign-status" style="margin-left:10px;"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>Website aktualisieren</h2>
|
||||||
|
<p>Laedt das neueste Docker-Image und startet den Website-Container neu.</p>
|
||||||
|
<button id="sync-site">Sync starten</button>
|
||||||
|
<span id="sync-status" style="margin-left:10px;"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<h2>Eigenes Passwort aendern</h2>
|
<h2>Eigenes Passwort aendern</h2>
|
||||||
<form id="admin-pw-form" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
<form id="admin-pw-form" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
||||||
|
|||||||
@@ -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 ====================
|
// ==================== EIGENES PASSWORT ====================
|
||||||
|
|
||||||
$('#admin-pw-form').submit(function(e) {
|
$('#admin-pw-form').submit(function(e) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken');
|
|||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { execFile } = require('child_process');
|
||||||
const db = require('./database');
|
const db = require('./database');
|
||||||
|
|
||||||
const app = express();
|
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 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 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 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;
|
const IMAGE_RE = /\.(jpg|jpeg|png|gif|webp)$/i;
|
||||||
|
let syncInProgress = false;
|
||||||
|
|
||||||
console.log('Using full-size images at', FULLS_DIR);
|
console.log('Using full-size images at', FULLS_DIR);
|
||||||
console.log('Using thumbnails at', THUMBS_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 ====================
|
// ==================== AUTH API ====================
|
||||||
|
|
||||||
app.post('/api/auth/login', (req, res) => {
|
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 ====================
|
// ==================== STATIC FILES ====================
|
||||||
|
|
||||||
app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage);
|
app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ services:
|
|||||||
- INITIAL_ADMIN_PASSWORD=Start123
|
- INITIAL_ADMIN_PASSWORD=Start123
|
||||||
- FULLS_DIR=/app/public/images/fulls
|
- FULLS_DIR=/app/public/images/fulls
|
||||||
- THUMBS_DIR=/app/public/images/thumbs
|
- THUMBS_DIR=/app/public/images/thumbs
|
||||||
|
- SYNC_IMAGE=ghcr.io/kroonk/photography:latest
|
||||||
|
- SYNC_CONTAINER=photography-website
|
||||||
volumes:
|
volumes:
|
||||||
# Nextcloud-Ordner mit Originalbildern (read-only)
|
# Nextcloud-Ordner mit Originalbildern (read-only)
|
||||||
- /volume1/nextcloud/data/Tom/files/Websitebilder:/app/public/images/fulls:ro
|
- /volume1/nextcloud/data/Tom/files/Websitebilder:/app/public/images/fulls:ro
|
||||||
|
|||||||
Reference in New Issue
Block a user