mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-14 00:32:35 +10:00
5
server/src/config/cache.config.ts
Normal file
5
server/src/config/cache.config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('cache', () => ({
|
||||
pdfDeletionTime: parseInt(process.env.PDF_DELETION_TIME, 10),
|
||||
}));
|
||||
@ -4,6 +4,7 @@ import Joi from 'joi';
|
||||
|
||||
import appConfig from './app.config';
|
||||
import authConfig from './auth.config';
|
||||
import cacheConfig from './cache.config';
|
||||
import databaseConfig from './database.config';
|
||||
import googleConfig from './google.config';
|
||||
import mailConfig from './mail.config';
|
||||
@ -52,12 +53,15 @@ const validationSchema = Joi.object({
|
||||
STORAGE_URL_PREFIX: Joi.string().allow(''),
|
||||
STORAGE_ACCESS_KEY: Joi.string().allow(''),
|
||||
STORAGE_SECRET_KEY: Joi.string().allow(''),
|
||||
|
||||
// Cache
|
||||
PDF_DELETION_TIME: Joi.number().default(6 * 24 * 60 * 60 * 1000), // 6 days
|
||||
});
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestConfigModule.forRoot({
|
||||
load: [appConfig, authConfig, databaseConfig, googleConfig, mailConfig, storageConfig],
|
||||
load: [appConfig, authConfig, cacheConfig, databaseConfig, googleConfig, mailConfig, storageConfig],
|
||||
validationSchema: validationSchema,
|
||||
}),
|
||||
],
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
|
||||
import { PrinterService } from './printer.service';
|
||||
|
||||
@ -7,7 +7,7 @@ export class PrinterController {
|
||||
constructor(private readonly printerService: PrinterService) {}
|
||||
|
||||
@Get('/:username/:slug')
|
||||
printAsPdf(@Param('username') username: string, @Param('slug') slug: string): Promise<string> {
|
||||
return this.printerService.printAsPdf(username, slug);
|
||||
printAsPdf(@Param('username') username: string, @Param('slug') slug: string, @Query('lastUpdated') lastUpdated: string): Promise<string> {
|
||||
return this.printerService.printAsPdf(username, slug, lastUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,14 +2,11 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { PageConfig } from '@reactive-resume/schema';
|
||||
import { mkdir, unlink, writeFile } from 'fs/promises';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { access, mkdir, readdir, unlink, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { Browser, chromium } from 'playwright-chromium';
|
||||
|
||||
export const DELETION_TIME = 10 * 1000; // 10 seconds
|
||||
|
||||
@Injectable()
|
||||
export class PrinterService implements OnModuleInit, OnModuleDestroy {
|
||||
private browser: Browser;
|
||||
@ -26,68 +23,92 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy {
|
||||
await this.browser.close();
|
||||
}
|
||||
|
||||
async printAsPdf(username: string, slug: string): Promise<string> {
|
||||
const url = this.configService.get<string>('app.url');
|
||||
async printAsPdf(username: string, slug: string, lastUpdated: string): Promise<string> {
|
||||
const serverUrl = this.configService.get<string>('app.serverUrl');
|
||||
const secretKey = this.configService.get<string>('app.secretKey');
|
||||
|
||||
const page = await this.browser.newPage();
|
||||
|
||||
await page.goto(`${url}/${username}/${slug}/printer?secretKey=${secretKey}`);
|
||||
await page.waitForSelector('html.wf-active');
|
||||
|
||||
const pageFormat: PageConfig['format'] = await page.$$eval(
|
||||
'[data-page]',
|
||||
(pages) => pages[0].getAttribute('data-format') as PageConfig['format']
|
||||
);
|
||||
|
||||
const resumePages = await page.$$eval('[data-page]', (pages) =>
|
||||
pages.map((page, index) => ({
|
||||
pageNumber: index + 1,
|
||||
innerHTML: page.innerHTML,
|
||||
height: page.clientHeight,
|
||||
}))
|
||||
);
|
||||
|
||||
const pdf = await PDFDocument.create();
|
||||
const directory = join(__dirname, '..', 'assets/exports');
|
||||
const filename = `RxResume_PDFExport_${nanoid()}.pdf`;
|
||||
const filename = `RxResume_PDFExport_${username}_${slug}_${lastUpdated}.pdf`;
|
||||
const publicUrl = `${serverUrl}/assets/exports/${filename}`;
|
||||
|
||||
for (let index = 0; index < resumePages.length; index++) {
|
||||
await page.evaluate((page) => (document.body.innerHTML = page.innerHTML), resumePages[index]);
|
||||
|
||||
const buffer = await page.pdf({
|
||||
printBackground: true,
|
||||
height: resumePages[index].height,
|
||||
width: pageFormat === 'A4' ? '210mm' : '216mm',
|
||||
try {
|
||||
// check if file already exists
|
||||
await access(join(directory, filename));
|
||||
} catch {
|
||||
// delete old files and scheduler jobs
|
||||
const activeSchedulerTimeouts = this.schedulerRegistry.getTimeouts();
|
||||
await readdir(directory).then(async (files) => {
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
if (file.startsWith(`RxResume_PDFExport_${username}_${slug}`)) {
|
||||
await unlink(join(directory, file));
|
||||
if (activeSchedulerTimeouts[`delete-${file}`]) {
|
||||
this.schedulerRegistry.deleteTimeout(`delete-${file}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const pageDoc = await PDFDocument.load(buffer);
|
||||
const copiedPages = await pdf.copyPages(pageDoc, [0]);
|
||||
// create file as it doesn't exist
|
||||
const url = this.configService.get<string>('app.url');
|
||||
const secretKey = this.configService.get<string>('app.secretKey');
|
||||
const pdfDeletionTime = this.configService.get<number>('cache.pdfDeletionTime');
|
||||
|
||||
copiedPages.forEach((copiedPage) => pdf.addPage(copiedPage));
|
||||
}
|
||||
const page = await this.browser.newPage();
|
||||
|
||||
await page.close();
|
||||
await page.goto(`${url}/${username}/${slug}/printer?secretKey=${secretKey}`);
|
||||
await page.waitForSelector('html.wf-active');
|
||||
|
||||
const pdfBytes = await pdf.save();
|
||||
const pageFormat: PageConfig['format'] = await page.$$eval(
|
||||
'[data-page]',
|
||||
(pages) => pages[0].getAttribute('data-format') as PageConfig['format']
|
||||
);
|
||||
|
||||
await mkdir(directory, { recursive: true });
|
||||
await writeFile(join(directory, filename), pdfBytes);
|
||||
const resumePages = await page.$$eval('[data-page]', (pages) =>
|
||||
pages.map((page, index) => ({
|
||||
pageNumber: index + 1,
|
||||
innerHTML: page.innerHTML,
|
||||
height: page.clientHeight,
|
||||
}))
|
||||
);
|
||||
|
||||
// Delete PDF artifacts after DELETION_TIME ms
|
||||
const timeout = setTimeout(async () => {
|
||||
try {
|
||||
await unlink(join(directory, filename));
|
||||
const pdf = await PDFDocument.create();
|
||||
|
||||
this.schedulerRegistry.deleteTimeout(`delete-${filename}`);
|
||||
} catch {
|
||||
// pass through
|
||||
for (let index = 0; index < resumePages.length; index++) {
|
||||
await page.evaluate((page) => (document.body.innerHTML = page.innerHTML), resumePages[index]);
|
||||
|
||||
const buffer = await page.pdf({
|
||||
printBackground: true,
|
||||
height: resumePages[index].height,
|
||||
width: pageFormat === 'A4' ? '210mm' : '216mm',
|
||||
});
|
||||
|
||||
const pageDoc = await PDFDocument.load(buffer);
|
||||
const copiedPages = await pdf.copyPages(pageDoc, [0]);
|
||||
|
||||
copiedPages.forEach((copiedPage) => pdf.addPage(copiedPage));
|
||||
}
|
||||
}, DELETION_TIME);
|
||||
|
||||
this.schedulerRegistry.addTimeout(`delete-${filename}`, timeout);
|
||||
await page.close();
|
||||
|
||||
const pdfBytes = await pdf.save();
|
||||
|
||||
await mkdir(directory, { recursive: true });
|
||||
await writeFile(join(directory, filename), pdfBytes);
|
||||
|
||||
// Delete PDF artifacts after pdfDeletionTime ms
|
||||
const timeout = setTimeout(async () => {
|
||||
try {
|
||||
await unlink(join(directory, filename));
|
||||
|
||||
this.schedulerRegistry.deleteTimeout(`delete-${filename}`);
|
||||
} catch {
|
||||
// pass through
|
||||
}
|
||||
}, pdfDeletionTime);
|
||||
|
||||
this.schedulerRegistry.addTimeout(`delete-${filename}`, timeout);
|
||||
}
|
||||
|
||||
return publicUrl;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user