Files
instrucoes_gerais/clube67/newwhats.local/plugins/uploads/backend/routes.ts
T
VPS 4 Deploy Agent 45b7b91b04 fix(plugins): use conditional require for production Docker compatibility
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>
2026-05-11 08:34:34 +02:00

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;
}