Merge branch 'master' of github.com/Kroonk/Photography into master (Resolved docker-build.yml)
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 2m6s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 2m6s
This commit is contained in:
@@ -2,6 +2,7 @@ _site/
|
||||
.git/
|
||||
.claude/
|
||||
node_modules/
|
||||
images/
|
||||
.sass-cache/
|
||||
.jekyll-metadata
|
||||
*.md
|
||||
|
||||
29
.github/workflows/docker-build.yml
vendored
29
.github/workflows/docker-build.yml
vendored
@@ -1,15 +1,16 @@
|
||||
name: Build & Push Docker Image to GHCR
|
||||
name: Build & Push Docker Image to Gitea Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: kroonk/photography
|
||||
REGISTRY: git.mischlabs.de
|
||||
REGISTRY_IMAGE: git.mischlabs.de/mrdiderot/photography
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
if: ${{ github.server_url == 'https://git.mischlabs.de' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -40,16 +41,20 @@ jobs:
|
||||
fi
|
||||
docker --version
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.GHCR_USERNAME }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
- name: Log in to Gitea Package Registry
|
||||
shell: sh
|
||||
run: |
|
||||
if [ -z "${{ secrets.REGISTRY_USER }}" ] || [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||
echo "Missing registry credentials."
|
||||
echo "Create repository secrets REGISTRY_USER and REGISTRY_TOKEN."
|
||||
echo "REGISTRY_TOKEN must be a Gitea access token with package write permission."
|
||||
exit 1
|
||||
fi
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ env.REGISTRY }}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: Build and push Docker image
|
||||
shell: sh
|
||||
run: |
|
||||
docker build -t ghcr.io/kroonk/photography:latest -t ghcr.io/kroonk/photography:${{ github.sha }} .
|
||||
docker push ghcr.io/kroonk/photography:latest
|
||||
docker push ghcr.io/kroonk/photography:${{ github.sha }}
|
||||
docker build -t ${{ env.REGISTRY_IMAGE }}:latest -t ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} .
|
||||
docker push ${{ env.REGISTRY_IMAGE }}:latest
|
||||
docker push ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -12,19 +12,26 @@ COPY . .
|
||||
|
||||
RUN bundle exec jekyll build
|
||||
|
||||
# === Serve-Phase: node.js backend + ImageMagick ===
|
||||
FROM node:20-alpine
|
||||
# === Node-Dependencies: native sqlite3 mit Build-Tools bauen ===
|
||||
FROM node:20-alpine AS node-deps
|
||||
|
||||
# ImageMagick + Format-Delegates (JPEG/PNG/WebP), Build-Tools fuer sqlite3 native bindings
|
||||
RUN apk add --no-cache imagemagick \
|
||||
libjpeg-turbo libjpeg-turbo-dev \
|
||||
libpng libpng-dev \
|
||||
libwebp libwebp-dev \
|
||||
python3 make g++ build-base sqlite-dev
|
||||
RUN apk add --no-cache python3 make g++ sqlite-dev
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
# === Serve-Phase: node.js backend + Runtime-Tools ===
|
||||
FROM node:20-alpine
|
||||
|
||||
# Getrennte RUNs halten einzelne Registry-Layer klein genug fuer Proxy-Limits.
|
||||
RUN apk add --no-cache docker-cli
|
||||
RUN apk add --no-cache imagemagick libjpeg-turbo libpng libwebp
|
||||
RUN apk add --no-cache sqlite-libs
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
COPY --from=node-deps /app/node_modules ./node_modules
|
||||
|
||||
COPY backend ./backend
|
||||
COPY --from=builder /site/_site ./public
|
||||
|
||||
18
README.md
18
README.md
@@ -18,10 +18,18 @@ To deploy on a NAS or Linux server, adjust the `docker-compose.yml` to match you
|
||||
```yaml
|
||||
services:
|
||||
photography-website:
|
||||
image: ghcr.io/kroonk/photography:latest
|
||||
image: git.mischlabs.de/mrdiderot/photography:latest
|
||||
container_name: photography-website
|
||||
ports:
|
||||
- "8090:8090"
|
||||
environment:
|
||||
- JWT_SECRET=replace-with-a-long-random-secret
|
||||
- INITIAL_ADMIN_USERNAME=MrDiderot
|
||||
- INITIAL_ADMIN_PASSWORD=Start123
|
||||
- FULLS_DIR=/app/public/images/fulls
|
||||
- THUMBS_DIR=/app/public/images/thumbs
|
||||
- SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest
|
||||
- SYNC_CONTAINER=photography-website
|
||||
volumes:
|
||||
- /path/to/nextcloud/data:/app/public/images/fulls:ro
|
||||
- thumbs:/app/public/images/thumbs
|
||||
@@ -32,6 +40,14 @@ services:
|
||||
|
||||
Run `docker compose up -d` to launch the site.
|
||||
|
||||
Change the initial admin password after the first login. If you prefer not to store the initial password in `docker-compose.yml`, create a bcrypt hash and use `INITIAL_ADMIN_PASSWORD_HASH` instead:
|
||||
|
||||
```bash
|
||||
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).
|
||||
|
||||
22
admin.html
22
admin.html
@@ -6,7 +6,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<link rel="stylesheet" href="/assets/css/main.min.css" />
|
||||
<style>
|
||||
.admin-section { background: rgba(0,0,0,0.5); padding: 2em; margin-bottom: 2em; border-radius: 4px; }
|
||||
.admin-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25em; align-items: start; }
|
||||
.admin-section { background: rgba(0,0,0,0.5); padding: 1.5em; border-radius: 4px; min-width: 0; }
|
||||
.admin-section h2 { margin-top: 0; }
|
||||
.admin-section.wide { grid-column: 1 / -1; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.2); }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 0.8em; }
|
||||
@@ -23,6 +26,10 @@
|
||||
.folder-status.yes { color: #4CAF50; }
|
||||
.folder-status.no { color: #888; }
|
||||
#folder-table-wrap { display: none; margin-top: 1em; }
|
||||
@media screen and (max-width: 900px) {
|
||||
.admin-grid { grid-template-columns: 1fr; }
|
||||
.admin-section { padding: 1.25em; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="is-preload">
|
||||
@@ -34,7 +41,8 @@
|
||||
</ul></nav>
|
||||
</header>
|
||||
|
||||
<div id="main" style="display:block; padding: 2em; max-width: 1000px; margin: 0 auto; width: 100%;">
|
||||
<div id="main" style="display:block; padding: 2em; max-width: 1180px; margin: 0 auto; width: 100%;">
|
||||
<div class="admin-grid">
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Nutzer erstellen</h2>
|
||||
@@ -62,6 +70,13 @@
|
||||
</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 wide">
|
||||
<h2>Nutzerverwaltung</h2>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Username</th><th>Rolle</th><th>Erstellt</th><th>Aktionen</th></tr></thead>
|
||||
@@ -69,7 +84,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<div class="admin-section wide">
|
||||
<h2>Ordner-Zuweisung</h2>
|
||||
<p>Waehle einen Nutzer und klicke auf Ordner um sie zuzuweisen oder zu entfernen.</p>
|
||||
<select id="user-select">
|
||||
@@ -101,6 +116,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/js/jquery.min.js"></script>
|
||||
|
||||
@@ -33,32 +33,42 @@ $(document).ready(function() {
|
||||
|
||||
users.forEach(function(u) {
|
||||
var role = u.role || 'unbekannt';
|
||||
var badge = '<span class="badge badge-' + role + '">' + role + '</span>';
|
||||
var badge = $('<span></span>').addClass('badge badge-' + role).text(role);
|
||||
var created = u.created_at ? new Date(u.created_at).toLocaleDateString('de-DE') : '-';
|
||||
var info = created;
|
||||
|
||||
if (role === 'client' && u.expires_at) {
|
||||
var expDate = new Date(u.expires_at).toLocaleDateString('de-DE');
|
||||
if (u.expired) {
|
||||
badge = '<span class="badge badge-expired">abgelaufen</span>';
|
||||
badge = $('<span></span>').addClass('badge badge-expired').text('abgelaufen');
|
||||
info = 'Abgelaufen: ' + expDate;
|
||||
} else {
|
||||
info = 'Bis: ' + expDate;
|
||||
}
|
||||
}
|
||||
|
||||
var actions = '<button class="btn-small" onclick="changePassword(' + u.id + ', \'' + u.username.replace(/'/g, "\\'") + '\')">PW</button>';
|
||||
actions += '<button class="btn-small" onclick="deleteUser(' + u.id + ', \'' + u.username.replace(/'/g, "\\'") + '\')">X</button>';
|
||||
var row = $('<tr></tr>');
|
||||
var userCell = $('<td></td>').text(u.username + ' ').append(badge);
|
||||
var actions = $('<td></td>');
|
||||
$('<button type="button" class="btn-small user-password">PW</button>')
|
||||
.data('user-id', u.id)
|
||||
.data('username', u.username)
|
||||
.appendTo(actions);
|
||||
$('<button type="button" class="btn-small user-delete">X</button>')
|
||||
.data('user-id', u.id)
|
||||
.data('username', u.username)
|
||||
.appendTo(actions);
|
||||
|
||||
$('#users-tbody').append(
|
||||
'<tr><td>' + u.id + '</td><td>' + u.username + ' ' + badge +
|
||||
'</td><td>' + role + '</td><td>' + info +
|
||||
'</td><td>' + actions + '</td></tr>'
|
||||
);
|
||||
row.append($('<td></td>').text(u.id));
|
||||
row.append(userCell);
|
||||
row.append($('<td></td>').text(role));
|
||||
row.append($('<td></td>').text(info));
|
||||
row.append(actions);
|
||||
$('#users-tbody').append(row);
|
||||
|
||||
// Dropdown fuer Ordner-Zuweisung (nur non-admin)
|
||||
if (role !== 'admin') {
|
||||
$('#user-select').append('<option value="' + u.id + '">' + u.username + ' (' + role + ')</option>');
|
||||
$('#user-select').append($('<option></option>').val(u.id).text(u.username + ' (' + role + ')'));
|
||||
}
|
||||
});
|
||||
}).fail(function(xhr) {
|
||||
@@ -96,16 +106,20 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
// Nutzer loeschen
|
||||
window.deleteUser = function(id, name) {
|
||||
function deleteUser(id, name) {
|
||||
if (confirm('Nutzer "' + name + '" wirklich loeschen?')) {
|
||||
$.ajax({ url: '/api/admin/users/' + id, type: 'DELETE' })
|
||||
.done(loadUsers)
|
||||
.fail(function(err) { alert(err.responseJSON ? err.responseJSON.error : 'Fehler'); });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$('#users-tbody').on('click', '.user-delete', function() {
|
||||
deleteUser($(this).data('user-id'), $(this).data('username'));
|
||||
});
|
||||
|
||||
// Passwort aendern (Admin setzt fuer anderen User)
|
||||
window.changePassword = function(id, name) {
|
||||
function changePassword(id, name) {
|
||||
var newPw = prompt('Neues Passwort fuer "' + name + '":');
|
||||
if (!newPw) return;
|
||||
$.ajax({
|
||||
@@ -118,7 +132,11 @@ $(document).ready(function() {
|
||||
}).fail(function(err) {
|
||||
alert(err.responseJSON ? err.responseJSON.error : 'Fehler');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
$('#users-tbody').on('click', '.user-password', function() {
|
||||
changePassword($(this).data('user-id'), $(this).data('username'));
|
||||
});
|
||||
|
||||
// ==================== ORDNER-ZUWEISUNG ====================
|
||||
|
||||
@@ -127,7 +145,8 @@ $(document).ready(function() {
|
||||
|
||||
function loadFolders() {
|
||||
$.get('/api/admin/folders').done(function(folders) {
|
||||
allFolders = folders;
|
||||
allFolders = folders || [];
|
||||
renderFolderTable();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,6 +228,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) {
|
||||
|
||||
@@ -46,13 +46,44 @@ db.serialize(() => {
|
||||
UNIQUE(user_id, image_name)
|
||||
)`);
|
||||
|
||||
// Default-Admin erstellen wenn keiner existiert
|
||||
db.get('SELECT * FROM users WHERE username = ?', ['admin'], (err, row) => {
|
||||
if (!row) {
|
||||
const hash = bcrypt.hashSync('admin123', 10);
|
||||
db.run('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', ['admin', hash, 'admin']);
|
||||
console.log('Default admin created: admin / admin123');
|
||||
// Initialen Admin nur mit expliziten Credentials aus der Umgebung anlegen.
|
||||
db.serialize(() => {
|
||||
const username = process.env.INITIAL_ADMIN_USERNAME;
|
||||
const password = process.env.INITIAL_ADMIN_PASSWORD;
|
||||
const passwordHash = process.env.INITIAL_ADMIN_PASSWORD_HASH || (password ? bcrypt.hashSync(password, 10) : null);
|
||||
if (!username || !passwordHash) {
|
||||
console.warn('Set INITIAL_ADMIN_USERNAME with INITIAL_ADMIN_PASSWORD or INITIAL_ADMIN_PASSWORD_HASH to bootstrap an admin.');
|
||||
return;
|
||||
}
|
||||
|
||||
db.get('SELECT id FROM users WHERE username = ?', [username], (err, row) => {
|
||||
if (err || row) return;
|
||||
|
||||
db.run(
|
||||
'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)',
|
||||
[username, passwordHash, 'admin'],
|
||||
(insertErr) => {
|
||||
if (insertErr) {
|
||||
console.error('Failed to create initial admin:', insertErr.message);
|
||||
return;
|
||||
}
|
||||
console.log(`Initial admin created: ${username}`);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
db.get('SELECT id, role FROM users WHERE username = ?', [username], (err, row) => {
|
||||
if (err || !row || row.role === 'admin') return;
|
||||
|
||||
db.run('UPDATE users SET role = ? WHERE id = ?', ['admin', row.id], (updateErr) => {
|
||||
if (updateErr) {
|
||||
console.error('Failed to promote initial admin:', updateErr.message);
|
||||
return;
|
||||
}
|
||||
console.log(`Initial admin promoted: ${username}`);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,13 +4,29 @@ const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
const archiver = require('archiver');
|
||||
const db = require('./database');
|
||||
|
||||
const app = express();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'mischkomposition-super-secret-key-change-me';
|
||||
const FULLS_DIR = path.join(__dirname, '../public/images/fulls');
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
if (!JWT_SECRET) {
|
||||
throw new Error('JWT_SECRET must be set before starting the server');
|
||||
}
|
||||
const PUBLIC_DIR = path.join(__dirname, '../public');
|
||||
const DEFAULT_FULLS_DIR = path.join(PUBLIC_DIR, 'images/fulls');
|
||||
const DEFAULT_THUMBS_DIR = path.join(PUBLIC_DIR, 'images/thumbs');
|
||||
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 || 'git.mischlabs.de/mrdiderot/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);
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
@@ -74,6 +90,61 @@ function listSubfolders(dir) {
|
||||
}
|
||||
}
|
||||
|
||||
function isSafePath(baseDir, requestedPath) {
|
||||
const resolvedBase = path.resolve(baseDir);
|
||||
const resolvedPath = path.resolve(resolvedBase, requestedPath);
|
||||
return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path.sep);
|
||||
}
|
||||
|
||||
function hasAssignedFolder(userId, folder, callback) {
|
||||
db.get(
|
||||
'SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
|
||||
[userId, folder],
|
||||
(err, row) => callback(!err && !!row)
|
||||
);
|
||||
}
|
||||
|
||||
function serveProtectedImage(req, res) {
|
||||
const kind = req.params[0];
|
||||
const relativePath = req.params[1] || '';
|
||||
const baseDir = kind === 'thumbs' ? THUMBS_DIR : FULLS_DIR;
|
||||
const segments = relativePath.split('/').filter(Boolean);
|
||||
const fileName = segments[segments.length - 1];
|
||||
|
||||
if (!fileName || !IMAGE_RE.test(fileName) || !isSafePath(baseDir, relativePath)) {
|
||||
return res.status(404).end();
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(baseDir, relativePath);
|
||||
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
||||
return res.status(404).end();
|
||||
}
|
||||
|
||||
// Bilder direkt im Root-Verzeichnis sind oeffentlich. Kundenordner sind geschuetzt.
|
||||
if (segments.length === 1) {
|
||||
return res.sendFile(absolutePath);
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not logged in' });
|
||||
}
|
||||
|
||||
if (req.user.role === 'admin') {
|
||||
return res.sendFile(absolutePath);
|
||||
}
|
||||
|
||||
hasAssignedFolder(req.user.id, segments[0], (allowed) => {
|
||||
if (!allowed) return res.status(403).json({ error: 'Forbidden' });
|
||||
res.sendFile(absolutePath);
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -256,6 +327,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);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== DOWNLOAD API ====================
|
||||
|
||||
function isValidName(name) {
|
||||
@@ -373,10 +479,12 @@ app.get('/api/download/folder/:folderName', (req, res) => {
|
||||
|
||||
// ==================== STATIC FILES ====================
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage);
|
||||
|
||||
app.use(express.static(PUBLIC_DIR));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
res.sendFile(path.join(PUBLIC_DIR, 'index.html'));
|
||||
});
|
||||
|
||||
const PORT = 8090;
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
services:
|
||||
photography:
|
||||
image: ghcr.io/kroonk/photography:latest
|
||||
image: git.mischlabs.de/mrdiderot/photography:latest
|
||||
container_name: photography-website
|
||||
ports:
|
||||
- "8090:8090"
|
||||
environment:
|
||||
- JWT_SECRET=${JWT_SECRET:?set JWT_SECRET in your environment}
|
||||
- INITIAL_ADMIN_USERNAME=MrDiderot
|
||||
- INITIAL_ADMIN_PASSWORD=Start123
|
||||
- FULLS_DIR=/app/public/images/fulls
|
||||
- THUMBS_DIR=/app/public/images/thumbs
|
||||
- SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest
|
||||
- SYNC_CONTAINER=photography-website
|
||||
volumes:
|
||||
# Nextcloud-Ordner mit Originalbildern (read-only)
|
||||
- /volume1/nextcloud/data/Tom/files/Websitebilder:/app/public/images/fulls:ro
|
||||
@@ -24,7 +32,7 @@ services:
|
||||
command: photography-website
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# GitHub Login von deinem NAS uebernehmen, um ghcr.io lesen zu duerfen
|
||||
# Registry Login von deinem NAS uebernehmen, um private Images lesen zu duerfen
|
||||
- ~/.docker/config.json:/config.json:ro
|
||||
environment:
|
||||
- WATCHTOWER_CLEANUP=true
|
||||
|
||||
Reference in New Issue
Block a user