333 lines
12 KiB
JavaScript
333 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Property Brochure PDF Generator API - A4 Scale
|
|
* Uses consistent A4 format with proper scaling
|
|
*
|
|
* NEW FLOW: Supports both download links and direct file responses
|
|
* - return_download_link: true -> Generate PDF, save on server, return download link
|
|
* - return_download_link: false -> Generate PDF, return base64 content (old behavior)
|
|
*/
|
|
|
|
const express = require('express');
|
|
const puppeteer = require('puppeteer');
|
|
const bodyParser = require('body-parser');
|
|
const cors = require('cors');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 8000;
|
|
|
|
// Configure temp folder
|
|
const TEMP_FOLDER = 'temp';
|
|
if (!fs.existsSync(TEMP_FOLDER)) {
|
|
fs.mkdirSync(TEMP_FOLDER, { recursive: true });
|
|
}
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(bodyParser.json({ limit: '100mb' }));
|
|
app.use(bodyParser.urlencoded({ extended: true, limit: '100mb' }));
|
|
|
|
// Serve static files from temp folder
|
|
app.use('/download', express.static(TEMP_FOLDER));
|
|
|
|
/**
|
|
* Generate A4 PDF with proper scaling
|
|
*/
|
|
async function generateA4PDF(htmlContent, outputPDF) {
|
|
let browser = null;
|
|
|
|
try {
|
|
console.log("🚀 Starting PDF generation...");
|
|
|
|
// Launch browser
|
|
browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-accelerated-2d-canvas',
|
|
'--no-first-run',
|
|
'--no-zygote',
|
|
'--disable-gpu'
|
|
]
|
|
});
|
|
|
|
const page = await browser.newPage();
|
|
|
|
// Set viewport to A4 dimensions (210mm x 297mm = 794px x 1123px at 96 DPI)
|
|
await page.setViewport({ width: 794, height: 1123 });
|
|
page.setDefaultTimeout(0); // Infinite timeout
|
|
|
|
// Set content
|
|
await page.setContent(htmlContent, {
|
|
waitUntil: 'networkidle0',
|
|
timeout: 0 // Infinite timeout
|
|
});
|
|
|
|
// Wait for content to load completely
|
|
await page.waitForTimeout(0);
|
|
|
|
// Wait for all resources to load
|
|
try {
|
|
await page.waitForNetworkIdle({ timeout: 0 });
|
|
} catch (error) {
|
|
// Ignore timeout errors for networkidle
|
|
}
|
|
|
|
console.log("📄 Generating A4 PDF...");
|
|
|
|
const pdfOptions = {
|
|
path: outputPDF,
|
|
printBackground: true,
|
|
margin: {
|
|
top: '0.5in',
|
|
right: '0.5in',
|
|
bottom: '0.5in',
|
|
left: '0.5in'
|
|
},
|
|
scale: 1.0,
|
|
format: 'A4', // Always A4
|
|
preferCSSPageSize: false,
|
|
displayHeaderFooter: false,
|
|
omitBackground: false
|
|
};
|
|
|
|
// Generate PDF
|
|
await page.pdf(pdfOptions);
|
|
await browser.close();
|
|
|
|
// Get PDF file size for logging (no size restriction for download links)
|
|
const pdfStats = fs.statSync(outputPDF);
|
|
const pdfSizeMB = pdfStats.size / (1024 * 1024);
|
|
|
|
console.log(`✅ A4 PDF generated: ${outputPDF}`);
|
|
console.log(`📐 Format: A4 (210mm x 297mm)`);
|
|
console.log(`📏 Margins: 0.5in all sides`);
|
|
console.log(`🔍 Scale: 1.0`);
|
|
console.log(`📊 File size: ${pdfSizeMB.toFixed(2)} MB`);
|
|
|
|
} catch (error) {
|
|
if (error.name === 'TimeoutError') {
|
|
throw new Error("Timeout: Page took too long to load.");
|
|
}
|
|
console.error(`❌ PDF generation error: ${error.message}`);
|
|
throw error;
|
|
} finally {
|
|
// Cleanup
|
|
if (browser) {
|
|
try {
|
|
await browser.close();
|
|
} catch (error) {
|
|
console.error('Error closing browser:', error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main PDF generation endpoint - supports both download link and base64 response
|
|
*/
|
|
app.post('/generate-pdf', async (req, res) => {
|
|
try {
|
|
// Get request data
|
|
let inputContent = null;
|
|
let outputName = null;
|
|
let returnDownloadLink = false; // Default to old behavior
|
|
|
|
if (req.is('application/json')) {
|
|
try {
|
|
const data = req.body;
|
|
if (data) {
|
|
// Handle different request formats
|
|
if (data.input) {
|
|
// Direct HTML input
|
|
inputContent = data.input;
|
|
outputName = data.output || null;
|
|
returnDownloadLink = data.return_download_link || false;
|
|
} else if (data.html) {
|
|
// Alternative HTML field
|
|
inputContent = data.html;
|
|
outputName = data.filename || null;
|
|
returnDownloadLink = data.return_download_link || false;
|
|
} else if (data.content) {
|
|
// Content field
|
|
inputContent = data.content;
|
|
outputName = data.name || null;
|
|
returnDownloadLink = data.return_download_link || false;
|
|
}
|
|
}
|
|
} catch (parseError) {
|
|
console.error('JSON parse error:', parseError);
|
|
return res.status(400).json({ error: 'Invalid JSON in request body' });
|
|
}
|
|
} else if (req.is('application/x-www-form-urlencoded')) {
|
|
// Handle form data
|
|
inputContent = req.body.input || req.body.html || req.body.content;
|
|
outputName = req.body.output || req.body.filename || req.body.name;
|
|
returnDownloadLink = req.body.return_download_link === 'true' || req.body.return_download_link === true;
|
|
} else {
|
|
// Try to get from raw body
|
|
try {
|
|
const rawData = JSON.parse(req.body);
|
|
inputContent = rawData.input || rawData.html || rawData.content;
|
|
outputName = rawData.output || rawData.filename || rawData.name;
|
|
returnDownloadLink = rawData.return_download_link || false;
|
|
} catch (error) {
|
|
return res.status(400).json({ error: 'Invalid request format' });
|
|
}
|
|
}
|
|
|
|
// Validate input
|
|
if (!inputContent) {
|
|
return res.status(400).json({ error: 'No HTML content provided' });
|
|
}
|
|
|
|
console.log(`📥 Received request - Download Link: ${returnDownloadLink}`);
|
|
|
|
// Generate unique filename
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const randomId = crypto.randomBytes(4).toString('hex');
|
|
const filename = outputName ?
|
|
`${outputName.replace(/[^a-zA-Z0-9-_]/g, '_')}_${randomId}.pdf` :
|
|
`property_brochure_${randomId}.pdf`;
|
|
|
|
const outputPDF = path.join(TEMP_FOLDER, filename);
|
|
|
|
// Generate PDF
|
|
await generateA4PDF(inputContent, outputPDF);
|
|
|
|
if (returnDownloadLink) {
|
|
// NEW FLOW: Return download link
|
|
const downloadUrl = `https://salesforce.tech4biz.io/download/${filename}`;
|
|
const pdfStats = fs.statSync(outputPDF);
|
|
const pdfSizeMB = pdfStats.size / (1024 * 1024);
|
|
|
|
const result = {
|
|
success: true,
|
|
message: "PDF generated successfully",
|
|
download_url: downloadUrl,
|
|
filename: filename,
|
|
file_size_mb: pdfSizeMB.toFixed(2),
|
|
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
|
|
generated_at: new Date().toISOString(),
|
|
pdf_id: randomId,
|
|
status: "download_ready"
|
|
};
|
|
|
|
console.log(`🔗 PDF ready for download: ${downloadUrl}`);
|
|
console.log(`📊 File size: ${pdfSizeMB.toFixed(2)} MB`);
|
|
|
|
res.json(result);
|
|
} else {
|
|
// OLD FLOW: Return base64 content
|
|
const pdfBuffer = fs.readFileSync(outputPDF);
|
|
const base64Content = pdfBuffer.toString('base64');
|
|
|
|
// Clean up the file
|
|
if (fs.existsSync(outputPDF)) {
|
|
fs.unlinkSync(outputPDF);
|
|
}
|
|
|
|
res.setHeader('Content-Type', 'application/pdf');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
res.send(Buffer.from(base64Content, 'base64'));
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`❌ API Error: ${error.message}`);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Download endpoint for PDF files
|
|
*/
|
|
app.get('/download/:filename', (req, res) => {
|
|
try {
|
|
const filename = req.params.filename;
|
|
const filePath = path.join(TEMP_FOLDER, filename);
|
|
|
|
// Security check - ensure filename is safe
|
|
if (!filename.match(/^[a-zA-Z0-9._-]+\.pdf$/)) {
|
|
return res.status(400).json({ error: 'Invalid filename' });
|
|
}
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
return res.status(404).json({ error: 'PDF file not found or expired' });
|
|
}
|
|
|
|
// Send file response
|
|
res.setHeader('Content-Type', 'application/pdf');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${path.basename(filePath)}"`);
|
|
|
|
const fileStream = fs.createReadStream(filePath);
|
|
fileStream.pipe(res);
|
|
|
|
// Keep the generated PDF file (don't delete it)
|
|
fileStream.on('end', () => {
|
|
console.log(`📥 PDF downloaded: ${filePath}`);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(`❌ Download Error: ${error.message}`);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Health check endpoint
|
|
*/
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'healthy',
|
|
timestamp: new Date().toISOString(),
|
|
version: '7.0.0',
|
|
features: ['download_links', 'base64_response', 'a4_format', 'infinite_timeouts']
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Clean up temporary HTML files older than 1 hour (keep PDF files)
|
|
*/
|
|
function cleanupTempFiles() {
|
|
try {
|
|
const currentTime = Date.now();
|
|
const files = fs.readdirSync(TEMP_FOLDER);
|
|
|
|
files.forEach(filename => {
|
|
const filepath = path.join(TEMP_FOLDER, filename);
|
|
const stats = fs.statSync(filepath);
|
|
|
|
// Only clean up HTML files, keep PDF files
|
|
if (stats.isFile() && filename.endsWith('.html') && (currentTime - stats.mtime.getTime()) > 3600000) { // 1 hour
|
|
try {
|
|
fs.unlinkSync(filepath);
|
|
console.log(`🗑️ Cleaned up old HTML file: ${filename}`);
|
|
} catch (error) {
|
|
console.error(`Error deleting file ${filename}:`, error);
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error during cleanup:', error);
|
|
}
|
|
}
|
|
|
|
// Clean up old files every hour
|
|
setInterval(cleanupTempFiles, 3600000);
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
console.log(`🚀 PDF Generator API running on port ${PORT}`);
|
|
console.log(`📁 Temp folder: ${TEMP_FOLDER}`);
|
|
console.log(`🔗 Download endpoint: http://localhost:${PORT}/download/:filename`);
|
|
console.log(`📊 Health check: http://localhost:${PORT}/health`);
|
|
console.log(`✨ Features: Download links, Base64 response, A4 format, Infinite timeouts`);
|
|
});
|
|
|
|
module.exports = app;
|