Bilder-Download: Mehrfach-Auswahl + Ordner-ZIP
- Selection-Mode: Bilder in Galerie per Klick auswaehlen - 1-3 Bilder: einzeln herunterladen, 4+: als ZIP - Ordner-Download: ganzen zugewiesenen Ordner als ZIP - Sicherheit: Zugriffspruefung + Path-Traversal-Schutz - archiver Package fuer Streaming-ZIP-Erstellung Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const archiver = require('archiver');
|
||||
const db = require('./database');
|
||||
|
||||
const app = express();
|
||||
@@ -255,6 +256,121 @@ app.post('/api/admin/assign', requireAdmin, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== DOWNLOAD API ====================
|
||||
|
||||
function isValidName(name) {
|
||||
return name && !name.includes('/') && !name.includes('\\') && !name.includes('..');
|
||||
}
|
||||
|
||||
app.post('/api/download/selected', (req, res) => {
|
||||
const { images } = req.body;
|
||||
if (!Array.isArray(images) || images.length === 0) {
|
||||
return res.status(400).json({ error: 'Keine Bilder ausgewaehlt' });
|
||||
}
|
||||
|
||||
// Alle Bilder validieren und Zugriff pruefen
|
||||
let pending = images.length;
|
||||
const verified = [];
|
||||
let responded = false;
|
||||
|
||||
images.forEach((img, idx) => {
|
||||
if (responded) return;
|
||||
const name = img.name;
|
||||
const folder = img.folder || null;
|
||||
|
||||
// Dateiname + Ordnername validieren
|
||||
if (!isValidName(name) || !IMAGE_RE.test(name) || (folder && !isValidName(folder))) {
|
||||
responded = true;
|
||||
return res.status(400).json({ error: 'Ungueltiger Dateiname' });
|
||||
}
|
||||
|
||||
const checkAccess = (ok) => {
|
||||
if (responded) return;
|
||||
if (!ok) {
|
||||
responded = true;
|
||||
return res.status(403).json({ error: 'Kein Zugriff auf: ' + name });
|
||||
}
|
||||
verified[idx] = img;
|
||||
pending--;
|
||||
if (pending === 0) sendFiles();
|
||||
};
|
||||
|
||||
if (!folder) {
|
||||
// Root-Bild: fuer alle zugaenglich
|
||||
const filePath = path.join(FULLS_DIR, name);
|
||||
checkAccess(fs.existsSync(filePath));
|
||||
} else {
|
||||
// Ordner-Bild: nur wenn User den Ordner zugewiesen hat
|
||||
if (!req.user) return checkAccess(false);
|
||||
db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
|
||||
[req.user.id, folder], (err, row) => {
|
||||
if (err || !row) return checkAccess(false);
|
||||
const filePath = path.join(FULLS_DIR, folder, name);
|
||||
checkAccess(fs.existsSync(filePath));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function sendFiles() {
|
||||
if (verified.length <= 3) {
|
||||
// Einzelne Downloads: URLs zurueckgeben
|
||||
const urls = verified.map(img => {
|
||||
const prefix = img.folder ? encodeURIComponent(img.folder) + '/' : '';
|
||||
return '/images/fulls/' + prefix + encodeURIComponent(img.name);
|
||||
});
|
||||
return res.json({ mode: 'individual', urls });
|
||||
}
|
||||
|
||||
// 4+ Bilder: ZIP streamen
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="mischkomposition-auswahl.zip"');
|
||||
const archive = archiver('zip', { zlib: { level: 5 } });
|
||||
archive.on('error', () => {
|
||||
if (!res.headersSent) res.status(500).json({ error: 'ZIP-Fehler' });
|
||||
});
|
||||
archive.pipe(res);
|
||||
verified.forEach(img => {
|
||||
const filePath = img.folder
|
||||
? path.join(FULLS_DIR, img.folder, img.name)
|
||||
: path.join(FULLS_DIR, img.name);
|
||||
archive.file(filePath, { name: img.name });
|
||||
});
|
||||
archive.finalize();
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/download/folder/:folderName', (req, res) => {
|
||||
const folderName = req.params.folderName;
|
||||
if (!isValidName(folderName)) {
|
||||
return res.status(400).json({ error: 'Ungueltiger Ordnername' });
|
||||
}
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Nicht eingeloggt' });
|
||||
}
|
||||
db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
|
||||
[req.user.id, folderName], (err, row) => {
|
||||
if (err || !row) {
|
||||
return res.status(403).json({ error: 'Kein Zugriff auf diesen Ordner' });
|
||||
}
|
||||
const folderPath = path.join(FULLS_DIR, folderName);
|
||||
const images = listImagesInDir(folderPath);
|
||||
if (images.length === 0) {
|
||||
return res.status(404).json({ error: 'Keine Bilder im Ordner' });
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="' + encodeURIComponent(folderName) + '.zip"');
|
||||
const archive = archiver('zip', { zlib: { level: 5 } });
|
||||
archive.on('error', () => {
|
||||
if (!res.headersSent) res.status(500).json({ error: 'ZIP-Fehler' });
|
||||
});
|
||||
archive.pipe(res);
|
||||
images.forEach(name => {
|
||||
archive.file(path.join(folderPath, name), { name });
|
||||
});
|
||||
archive.finalize();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== STATIC FILES ====================
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
|
||||
Reference in New Issue
Block a user