45b7b91b04
Replace static require('backend/src/...') with NODE_ENV-conditional
requires in compiled plugin JS and TypeScript sources.
In production Docker /app/backend/src/ does not exist — only /app/dist/.
Fixes MODULE_NOT_FOUND errors on UnifiedStorageProvider, ext-api,
leads, nanobana, and uploads plugins.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
162 lines
6.8 KiB
TypeScript
162 lines
6.8 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import multer from 'multer';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import { PluginContext } from '../../../backend/src/core/types';
|
|
|
|
export function createUploadRoutes(ctx: PluginContext): Router {
|
|
const router = Router();
|
|
const uploadDir = process.env.UPLOAD_DIR || path.join(process.cwd(), 'uploads');
|
|
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
|
|
|
const storage = multer.diskStorage({
|
|
destination: (_req, _file, cb) => cb(null, uploadDir),
|
|
filename: (_req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`),
|
|
});
|
|
const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 } });
|
|
|
|
// POST / — Upload file
|
|
router.post('/', upload.single('file'), async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.file) return res.status(400).json({ error: 'Nenhum arquivo enviado' });
|
|
|
|
const destination = req.query.destination as string;
|
|
const category = req.query.category as any || 'media';
|
|
|
|
let fileBuffer = fs.readFileSync(req.file.path);
|
|
let mimetype = req.file.mimetype;
|
|
|
|
// Emit transformation hook
|
|
const transformResults = await ctx.hooks.emit('upload:transform', {
|
|
buffer: fileBuffer,
|
|
mimetype: mimetype,
|
|
category: category,
|
|
originalname: req.file.originalname,
|
|
partnerId: req.body.partnerId
|
|
});
|
|
|
|
// If a plugin transformed the file, use the last result
|
|
if (transformResults && transformResults.length > 0) {
|
|
const lastResult = transformResults[transformResults.length - 1];
|
|
if (lastResult && lastResult.buffer) {
|
|
fileBuffer = lastResult.buffer;
|
|
mimetype = lastResult.mimetype || mimetype;
|
|
}
|
|
}
|
|
|
|
if (destination === 'cloud') {
|
|
if (fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
|
|
|
|
// Use a more robust import for the singleton (conditional for production Docker)
|
|
const storageCore = process.env.NODE_ENV === 'production'
|
|
? require('../../../../dist/core/StorageProvider')
|
|
: require('../../../../backend/src/core/StorageProvider');
|
|
const provider = storageCore.storageProvider || (storageCore.default && storageCore.default.storageProvider);
|
|
|
|
if (!provider) {
|
|
ctx.logger.error('[Uploads] StorageProvider not found in core');
|
|
return res.status(500).json({ error: 'Storage core unavailable' });
|
|
}
|
|
|
|
const cloudResult = await provider.uploadFile({
|
|
category,
|
|
partnerId: req.body.partnerId,
|
|
benefitId: req.body.benefitId,
|
|
postId: req.body.postId,
|
|
whatsappId: req.body.whatsappId,
|
|
file: {
|
|
originalname: req.file.originalname,
|
|
buffer: fileBuffer,
|
|
mimetype: mimetype
|
|
}
|
|
});
|
|
|
|
return res.json({
|
|
url: `https://${process.env.WASABI_BUCKET}.s3.wasabisys.com/${cloudResult.path}`,
|
|
filename: req.file.filename,
|
|
provider: 'wasabi'
|
|
});
|
|
}
|
|
|
|
// Local storage fallback: save the potentially transformed buffer
|
|
if (transformResults && transformResults.length > 0) {
|
|
fs.writeFileSync(req.file.path, fileBuffer);
|
|
}
|
|
|
|
const url = `/uploads/${req.file.filename}`;
|
|
await ctx.hooks.emit('upload:completed', { filename: req.file.filename, url });
|
|
res.json({ url, filename: req.file.filename, size: fileBuffer.length });
|
|
} catch (err: any) {
|
|
console.error('[Uploads] Error:', err);
|
|
res.status(500).json({ error: 'Erro no upload' });
|
|
}
|
|
});
|
|
|
|
// POST /logo — Upload partner logo
|
|
router.post('/logo', upload.single('logo'), async (req: Request, res: Response) => {
|
|
try {
|
|
if (!req.file) return res.status(400).json({ error: 'Nenhum logo enviado' });
|
|
|
|
let fileBuffer = fs.readFileSync(req.file.path);
|
|
let mimetype = req.file.mimetype;
|
|
|
|
// Emit transformation hook
|
|
const transformResults = await ctx.hooks.emit('upload:transform', {
|
|
buffer: fileBuffer,
|
|
mimetype: mimetype,
|
|
category: 'partners',
|
|
originalname: req.file.originalname
|
|
});
|
|
|
|
if (transformResults && transformResults.length > 0) {
|
|
const lastResult = transformResults[transformResults.length - 1];
|
|
if (lastResult && lastResult.buffer) {
|
|
fileBuffer = lastResult.buffer;
|
|
mimetype = lastResult.mimetype || mimetype;
|
|
// Overwrite local file with optimized version
|
|
fs.writeFileSync(req.file.path, fileBuffer);
|
|
}
|
|
}
|
|
|
|
const url = `/uploads/${req.file.filename}`;
|
|
res.json({ url, filename: req.file.filename });
|
|
} catch (err: any) {
|
|
console.error('[Uploads] Logo Error:', err);
|
|
res.status(500).json({ error: 'Erro no upload do logo' });
|
|
}
|
|
});
|
|
|
|
// DELETE /:filename — Delete file
|
|
router.delete('/:filename', async (req: Request, res: Response) => {
|
|
try {
|
|
const filePath = path.join(uploadDir, req.params.filename);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
await ctx.hooks.emit('upload:deleted', { filename: req.params.filename });
|
|
res.json({ message: 'Arquivo removido' });
|
|
} else {
|
|
res.status(404).json({ error: 'Arquivo não encontrado' });
|
|
}
|
|
} catch (err: any) { res.status(500).json({ error: 'Erro ao remover' }); }
|
|
});
|
|
|
|
// GET /:filename — Legacy access with Washabi Fallback 🛡️
|
|
router.get('/:filename', async (req: Request, res: Response) => {
|
|
const { filename } = req.params;
|
|
const localPath = path.join(uploadDir, filename);
|
|
|
|
// 🛡️ If exists locally, serve normally
|
|
if (fs.existsSync(localPath)) {
|
|
return res.sendFile(localPath);
|
|
}
|
|
|
|
// 🚀 If NOT local, redirect or proxy to UnifiedStorageProvider
|
|
// We know most legacy uploads go to the 'media' category in the proxy
|
|
const storageProxyUrl = `/api/storage/view/media/${filename}`;
|
|
ctx.logger.info(`[Uploads:Fallback] Redirecting legacy file ${filename} -> ${storageProxyUrl}`);
|
|
res.redirect(storageProxyUrl);
|
|
});
|
|
|
|
return router;
|
|
}
|