feat: integrate preview generation into upload flow

Task 4: Upload Routes Extended
- upload.js: Generate preview after single file upload
- batchUpload.js: Generate previews for all batch uploads
- Async preview generation (non-blocking response)
- Auto-update preview_path in database after generation

Task 5: Repository with preview_path
- GroupRepository: Include preview_path in INSERT
- getGroupById: Return previewPath in image objects
- groupFormatter: Add previewPath to formatGroupDetail()
- All queries now support preview_path column

Task 6: API Endpoints Extended
- Add PREVIEW_STATIC_DIRECTORY constant (/previews)
- Serve preview images via express.static
- All existing endpoints now return previewPath field
- Fallback to filePath if preview not available (frontend)
This commit is contained in:
Matthias Lotz 2025-10-30 20:41:06 +01:00
parent 940144cbf5
commit 661d6441ab
5 changed files with 78 additions and 12 deletions

View File

@ -2,6 +2,7 @@ const endpoints = {
UPLOAD_STATIC_DIRECTORY: '/upload',
UPLOAD_FILE: '/upload',
UPLOAD_BATCH: '/upload/batch',
PREVIEW_STATIC_DIRECTORY: '/previews',
DOWNLOAD_FILE: '/download/:id',
GET_GROUP: '/groups/:groupId',
GET_ALL_GROUPS: '/groups',

View File

@ -22,8 +22,8 @@ class GroupRepository {
if (groupData.images && groupData.images.length > 0) {
for (const image of groupData.images) {
await db.run(`
INSERT INTO images (group_id, file_name, original_name, file_path, upload_order, file_size, mime_type)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO images (group_id, file_name, original_name, file_path, upload_order, file_size, mime_type, preview_path)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, [
groupData.groupId,
image.fileName,
@ -31,7 +31,8 @@ class GroupRepository {
image.filePath,
image.uploadOrder,
image.fileSize || null,
image.mimeType || null
image.mimeType || null,
image.previewPath || null
]);
}
}
@ -67,6 +68,7 @@ class GroupRepository {
fileName: img.file_name,
originalName: img.original_name,
filePath: img.file_path,
previewPath: img.preview_path,
uploadOrder: img.upload_order,
fileSize: img.file_size,
mimeType: img.mime_type

View File

@ -5,6 +5,7 @@ const { endpoints } = require('../constants');
const UploadGroup = require('../models/uploadGroup');
const GroupRepository = require('../repositories/GroupRepository');
const dbManager = require('../database/DatabaseManager');
const ImagePreviewService = require('../services/ImagePreviewService');
const router = Router();
@ -64,6 +65,37 @@ router.post(endpoints.UPLOAD_BATCH, async (req, res) => {
});
}
// Generate previews for all uploaded images asynchronously
const previewDir = path.join(__dirname, '..', require('../constants').PREVIEW_FS_DIR);
const uploadDir = path.join(__dirname, '..', require('../constants').UPLOAD_FS_DIR);
// Generate previews in background (don't wait)
ImagePreviewService.generatePreviewsForGroup(
processedFiles.map(f => ({ file_name: f.fileName, file_path: `/upload/${f.fileName}` })),
uploadDir,
previewDir
).then(results => {
const successCount = results.filter(r => r.success).length;
console.log(`Preview generation completed: ${successCount}/${results.length} successful`);
// Update preview_path in database for successful previews
results.forEach(async (result) => {
if (result.success) {
try {
await dbManager.run(`
UPDATE images
SET preview_path = ?
WHERE group_id = ? AND file_name = ?
`, [result.previewPath, group.groupId, result.fileName]);
} catch (err) {
console.error(`Failed to update preview_path for ${result.fileName}:`, err);
}
}
});
}).catch(err => {
console.error('Preview generation failed:', err);
});
// Speichere Gruppe in SQLite
await GroupRepository.createGroup({
groupId: group.groupId,

View File

@ -1,15 +1,19 @@
const generateId = require("shortid");
const express = require('express');
const { Router } = require('express');
const { endpoints, UPLOAD_FS_DIR } = require('../constants');
const { endpoints, UPLOAD_FS_DIR, PREVIEW_FS_DIR } = require('../constants');
const path = require('path');
const ImagePreviewService = require('../services/ImagePreviewService');
const router = Router();
// Serve uploaded images via URL /upload but store files under data/images
router.use(endpoints.UPLOAD_STATIC_DIRECTORY, express.static( path.join(__dirname, '..', UPLOAD_FS_DIR) ));
router.post(endpoints.UPLOAD_FILE, (req, res) => {
// Serve preview images via URL /previews but store files under data/previews
router.use(endpoints.PREVIEW_STATIC_DIRECTORY, express.static( path.join(__dirname, '..', PREVIEW_FS_DIR) ));
router.post(endpoints.UPLOAD_FILE, async (req, res) => {
if(req.files === null){
console.log('No file uploaded');
return res.status(400).json({ msg: 'No file uploaded' });
@ -22,14 +26,40 @@ router.post(endpoints.UPLOAD_FILE, (req, res) => {
fileName = generateId() + '.' + fileEnding
const savePath = path.join(__dirname, '..', UPLOAD_FS_DIR, fileName);
file.mv(savePath, err => {
if(err) {
console.error(err);
return res.status(500).send(err);
}
try {
// Save the uploaded file
await new Promise((resolve, reject) => {
file.mv(savePath, err => {
if(err) reject(err);
else resolve();
});
});
res.json({ filePath: `${endpoints.UPLOAD_STATIC_DIRECTORY}/${fileName}`});
});
// Generate preview asynchronously (don't wait for it)
const previewFileName = ImagePreviewService._getPreviewFileName(fileName);
const previewPath = ImagePreviewService.getPreviewPath(previewFileName);
ImagePreviewService.generatePreview(savePath, previewPath)
.then(result => {
if (!result.success) {
console.warn(`Preview generation failed for ${fileName}:`, result.error);
}
})
.catch(err => {
console.error(`Unexpected error during preview generation for ${fileName}:`, err);
});
// Return immediately with file path
res.json({
filePath: `${endpoints.UPLOAD_STATIC_DIRECTORY}/${fileName}`,
fileName: fileName
});
} catch(err) {
console.error(err);
return res.status(500).send(err);
}
});
module.exports = router;

View File

@ -29,6 +29,7 @@ function formatGroupDetail(groupRow, images) {
fileName: img.file_name,
originalName: img.original_name,
filePath: img.file_path,
previewPath: img.preview_path || null,
uploadOrder: img.upload_order,
fileSize: img.file_size || null,
mimeType: img.mime_type || null