363 lines
14 KiB
JavaScript
363 lines
14 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const express_1 = require("express");
|
|
const client_1 = require("@prisma/client");
|
|
const multer_1 = __importStar(require("multer"));
|
|
const path_1 = __importDefault(require("path"));
|
|
const fs_1 = __importDefault(require("fs"));
|
|
const config_1 = require("../config/config");
|
|
const router = (0, express_1.Router)();
|
|
const prisma = new client_1.PrismaClient();
|
|
const getUploadsDir = (subPath) => {
|
|
const localUploadsDir = path_1.default.join(process.cwd(), 'uploads');
|
|
const legacyUploadsDir = path_1.default.join(process.cwd(), '../../uploads');
|
|
const baseDir = fs_1.default.existsSync(localUploadsDir)
|
|
? localUploadsDir
|
|
: legacyUploadsDir;
|
|
return subPath ? path_1.default.join(baseDir, subPath) : baseDir;
|
|
};
|
|
const loadRecipe = async (req, res, next) => {
|
|
try {
|
|
const { recipeId } = req.params;
|
|
if (!recipeId || isNaN(Number(recipeId))) {
|
|
return res.status(400).json({ success: false, message: 'Valid recipeId is required' });
|
|
}
|
|
const recipe = await prisma.recipe.findUnique({ where: { id: Number(recipeId) } });
|
|
if (!recipe) {
|
|
return res.status(404).json({ success: false, message: 'Recipe not found' });
|
|
}
|
|
req.recipeNumber = recipe.recipeNumber;
|
|
res.locals.recipe = recipe;
|
|
next();
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
const mimeExt = {
|
|
'image/jpeg': 'jpg',
|
|
'image/jpg': 'jpg',
|
|
'image/png': 'png',
|
|
'image/webp': 'webp'
|
|
};
|
|
const storage = multer_1.default.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
const recipeNumber = req.recipeNumber || req.body.recipeNumber || req.params.recipeNumber;
|
|
if (!recipeNumber) {
|
|
return cb(new Error('Recipe number is required'), '');
|
|
}
|
|
const uploadDir = getUploadsDir(recipeNumber);
|
|
try {
|
|
if (!fs_1.default.existsSync(uploadDir)) {
|
|
fs_1.default.mkdirSync(uploadDir, { recursive: true });
|
|
}
|
|
}
|
|
catch (e) {
|
|
return cb(new Error('Failed to prepare upload directory'), '');
|
|
}
|
|
cb(null, uploadDir);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
const recipeNumber = req.recipeNumber || req.body.recipeNumber || req.params.recipeNumber;
|
|
if (!recipeNumber) {
|
|
return cb(new Error('Recipe number is required'), '');
|
|
}
|
|
const uploadDir = getUploadsDir(recipeNumber);
|
|
const existingFiles = fs_1.default.existsSync(uploadDir)
|
|
? fs_1.default.readdirSync(uploadDir).filter(f => f.startsWith(`${recipeNumber}_`))
|
|
: [];
|
|
let maxIndex = -1;
|
|
existingFiles.forEach(f => {
|
|
const m = f.match(new RegExp(`^${recipeNumber}_(\\d+)`));
|
|
if (m && m[1] !== undefined) {
|
|
const idx = parseInt(m[1], 10);
|
|
if (!Number.isNaN(idx) && idx > maxIndex)
|
|
maxIndex = idx;
|
|
}
|
|
});
|
|
const nextIndex = maxIndex + 1;
|
|
const ext = mimeExt[file.mimetype] || 'jpg';
|
|
const filename = `${recipeNumber}_${nextIndex}.${ext}`;
|
|
cb(null, filename);
|
|
}
|
|
});
|
|
const upload = (0, multer_1.default)({
|
|
storage,
|
|
limits: {
|
|
fileSize: config_1.config.upload.maxFileSize,
|
|
},
|
|
fileFilter: (req, file, cb) => {
|
|
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
|
|
if (allowedTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
}
|
|
else {
|
|
cb(new Error('Invalid file type. Only JPEG, PNG and WebP are allowed.'));
|
|
}
|
|
},
|
|
});
|
|
router.post('/upload/:recipeId', loadRecipe, (req, res, next) => {
|
|
const mw = upload.array('images', 10);
|
|
mw(req, res, function (err) {
|
|
if (err) {
|
|
if (err instanceof multer_1.MulterError) {
|
|
return res.status(400).json({ success: false, message: `Upload error: ${err.message}` });
|
|
}
|
|
return res.status(400).json({ success: false, message: err.message || 'Upload failed' });
|
|
}
|
|
next();
|
|
});
|
|
}, async (req, res, next) => {
|
|
try {
|
|
const { recipeId } = req.params;
|
|
const files = req.files;
|
|
const recipe = res.locals.recipe;
|
|
if (!files || files.length === 0) {
|
|
return res.status(400).json({ success: false, message: 'No files uploaded' });
|
|
}
|
|
const images = await Promise.all(files.map(file => {
|
|
const relativePath = `uploads/${recipe.recipeNumber}/${file.filename}`;
|
|
return prisma.recipeImage.create({
|
|
data: { recipeId: Number(recipeId), filePath: relativePath }
|
|
});
|
|
}));
|
|
return res.status(201).json({
|
|
success: true,
|
|
data: images,
|
|
message: `${files.length} images uploaded successfully`,
|
|
});
|
|
}
|
|
catch (error) {
|
|
if (req.files) {
|
|
const files = req.files;
|
|
files.forEach(file => { if (fs_1.default.existsSync(file.path))
|
|
fs_1.default.unlinkSync(file.path); });
|
|
}
|
|
next(error);
|
|
}
|
|
});
|
|
router.delete('/:id', async (req, res, next) => {
|
|
try {
|
|
const { id } = req.params;
|
|
if (!id) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Image ID is required',
|
|
});
|
|
}
|
|
const image = await prisma.recipeImage.findUnique({
|
|
where: { id: parseInt(id) }
|
|
});
|
|
if (!image) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Image not found',
|
|
});
|
|
}
|
|
const fullPath = path_1.default.join(process.cwd(), '../..', image.filePath);
|
|
if (fs_1.default.existsSync(fullPath)) {
|
|
fs_1.default.unlinkSync(fullPath);
|
|
}
|
|
await prisma.recipeImage.delete({
|
|
where: { id: parseInt(id) }
|
|
});
|
|
return res.json({
|
|
success: true,
|
|
message: 'Image deleted successfully',
|
|
});
|
|
}
|
|
catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
router.get('/recipe/:recipeId', async (req, res, next) => {
|
|
try {
|
|
const { recipeId } = req.params;
|
|
if (!recipeId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Recipe ID is required',
|
|
});
|
|
}
|
|
const images = await prisma.recipeImage.findMany({
|
|
where: { recipeId: parseInt(recipeId) },
|
|
orderBy: { id: 'asc' }
|
|
});
|
|
return res.json({
|
|
success: true,
|
|
data: images,
|
|
});
|
|
}
|
|
catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
router.get('/serve/:imagePath(*)', (req, res, next) => {
|
|
try {
|
|
const imagePath = req.params.imagePath;
|
|
if (!imagePath) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Image path is required',
|
|
});
|
|
}
|
|
const cleanPath = imagePath.replace(/^uploads\//, '');
|
|
const fullPath = path_1.default.join(getUploadsDir(), cleanPath);
|
|
console.log(`Serving image: ${imagePath} -> ${fullPath}`);
|
|
if (!fs_1.default.existsSync(fullPath)) {
|
|
console.log(`Image not found: ${fullPath}`);
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Image not found',
|
|
requestedPath: imagePath,
|
|
resolvedPath: fullPath
|
|
});
|
|
}
|
|
const allowedOrigins = ['http://localhost:5173', 'http://localhost:3000'];
|
|
const origin = req.headers.origin;
|
|
const corsOrigin = process.env.CORS_ORIGIN === '*'
|
|
? (origin || '*')
|
|
: (origin && allowedOrigins.includes(origin)) ? origin : 'http://localhost:3000';
|
|
res.set({
|
|
'Access-Control-Allow-Origin': corsOrigin,
|
|
'Access-Control-Allow-Credentials': 'true',
|
|
'Cache-Control': 'public, max-age=31536000',
|
|
});
|
|
return res.sendFile(path_1.default.resolve(fullPath));
|
|
}
|
|
catch (error) {
|
|
console.error('Error serving image:', error);
|
|
next(error);
|
|
}
|
|
});
|
|
router.get('/:id', async (req, res, next) => {
|
|
try {
|
|
const { id } = req.params;
|
|
if (!id) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Image ID is required',
|
|
});
|
|
}
|
|
const image = await prisma.recipeImage.findUnique({
|
|
where: { id: parseInt(id) }
|
|
});
|
|
if (!image) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Image not found',
|
|
});
|
|
}
|
|
return res.json({
|
|
success: true,
|
|
data: image,
|
|
});
|
|
}
|
|
catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
router.post('/reorder/:recipeId', async (req, res, next) => {
|
|
try {
|
|
const { recipeId } = req.params;
|
|
const { order } = req.body;
|
|
if (!recipeId || isNaN(Number(recipeId))) {
|
|
return res.status(400).json({ success: false, message: 'Valid recipeId required' });
|
|
}
|
|
if (!Array.isArray(order) || order.length === 0) {
|
|
return res.status(400).json({ success: false, message: 'order (non-empty array) required' });
|
|
}
|
|
const rid = Number(recipeId);
|
|
const recipe = await prisma.recipe.findUnique({ where: { id: rid } });
|
|
if (!recipe)
|
|
return res.status(404).json({ success: false, message: 'Recipe not found' });
|
|
const images = await prisma.recipeImage.findMany({ where: { recipeId: rid }, orderBy: { id: 'asc' } });
|
|
if (images.length === 0)
|
|
return res.status(400).json({ success: false, message: 'No images to reorder' });
|
|
const existingIds = images.map(i => i.id).sort((a, b) => a - b);
|
|
const provided = [...order].sort((a, b) => a - b);
|
|
if (existingIds.length !== provided.length || !existingIds.every((v, i) => v === provided[i])) {
|
|
return res.status(400).json({ success: false, message: 'order must contain all image IDs exactly once' });
|
|
}
|
|
const recipeNumber = recipe.recipeNumber;
|
|
const baseDir = getUploadsDir(recipeNumber);
|
|
if (!fs_1.default.existsSync(baseDir)) {
|
|
return res.status(500).json({ success: false, message: 'Upload directory missing on server' });
|
|
}
|
|
const idToImage = new Map(images.map(i => [i.id, i]));
|
|
const newSequence = order.map((id, idx) => ({ idx, image: idToImage.get(id) }));
|
|
const tempRenames = [];
|
|
for (const { idx, image } of newSequence) {
|
|
const fileName = image.filePath.split('/').pop() || '';
|
|
const extMatch = fileName.match(/\.(jpg|jpeg|png|webp)$/i);
|
|
const ext = extMatch && extMatch[1] ? extMatch[1].toLowerCase() : 'jpg';
|
|
const finalName = `${recipeNumber}_${idx}.${ext === 'jpeg' ? 'jpg' : ext}`;
|
|
const oldFull = path_1.default.join(baseDir, path_1.default.basename(fileName));
|
|
const tempName = `${recipeNumber}__reorder_${idx}_${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`;
|
|
const tempFull = path_1.default.join(baseDir, tempName);
|
|
const finalFull = path_1.default.join(baseDir, finalName);
|
|
if (!fs_1.default.existsSync(oldFull)) {
|
|
return res.status(500).json({ success: false, message: `File missing on disk: ${fileName}` });
|
|
}
|
|
fs_1.default.renameSync(oldFull, tempFull);
|
|
tempRenames.push({ from: tempFull, to: finalFull, final: `uploads/${recipeNumber}/${finalName}`, idx, ext });
|
|
}
|
|
for (const r of tempRenames) {
|
|
fs_1.default.renameSync(r.from, r.to);
|
|
}
|
|
await Promise.all(newSequence.map(({ idx, image }) => {
|
|
const renameRecord = tempRenames.find(tr => tr.idx === idx);
|
|
return prisma.recipeImage.update({
|
|
where: { id: image.id },
|
|
data: { filePath: renameRecord.final }
|
|
});
|
|
}));
|
|
const updated = await prisma.recipeImage.findMany({ where: { recipeId: rid }, orderBy: { id: 'asc' } });
|
|
return res.json({
|
|
success: true,
|
|
message: 'Images reordered',
|
|
data: updated,
|
|
});
|
|
}
|
|
catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
exports.default = router;
|
|
//# sourceMappingURL=images.js.map
|