fix: use pdftocairo directly for thumbnails, no Docker image needed
Build and Deploy Verso / deploy (push) Successful in 15m11s
Build and Deploy Verso / deploy (push) Successful in 15m11s
The previous implementation delegated to ConversionManager which uses the Docker-based CommandRunner and is gated behind enablePdfConversions (ENABLE_PDF_CONVERSIONS env var). Neither is configured in the Verso deployment, so every thumbnail request 404'd before doing any work. poppler-utils (which provides pdftocairo) is already installed directly in the CLSI base image via install_deps.sh. Rewrite thumbnailFromBuild to call pdftocairo via execFile instead — no feature flag, no Docker image, no ConversionManager indirection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import crypto from 'node:crypto'
|
||||
import { execFile } from 'node:child_process'
|
||||
import os from 'node:os'
|
||||
import { promisify } from 'node:util'
|
||||
import logger from '@overleaf/logger'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import fs from 'node:fs/promises'
|
||||
@@ -16,6 +19,8 @@ import Settings from '@overleaf/settings'
|
||||
import Path from 'node:path'
|
||||
import { z } from '@overleaf/validation-tools'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
const CONVERSION_CONFIGS = {
|
||||
docx: { extension: 'docx' },
|
||||
markdown: { extension: 'zip' },
|
||||
@@ -251,9 +256,10 @@ async function convertProjectToDocument(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a JPEG thumbnail of page 1 of the compiled output.pdf using
|
||||
// pdftocairo (poppler-utils, installed in the CLSI base image). Does NOT
|
||||
// require ENABLE_PDF_CONVERSIONS or the pdftocairo Docker image.
|
||||
async function thumbnailFromBuild(req, res) {
|
||||
if (!Settings.enablePdfConversions) return res.sendStatus(404)
|
||||
|
||||
const { project_id: projectId, user_id: userId, build_id: buildId } = req.params
|
||||
if (!buildId?.match(OutputCacheManager.BUILD_REGEX)) return res.sendStatus(400)
|
||||
|
||||
@@ -272,20 +278,27 @@ async function thumbnailFromBuild(req, res) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const conversionId = crypto.randomUUID()
|
||||
let jpegPath
|
||||
try {
|
||||
jpegPath = await ConversionManager.promises.convertPDFToJPEGWithLock(
|
||||
conversionId,
|
||||
pdfPath,
|
||||
'thumbnail'
|
||||
)
|
||||
} catch (err) {
|
||||
logger.warn({ err, projectId, buildId }, 'thumbnail conversion failed')
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
const tmpDir = await fs.mkdtemp(Path.join(os.tmpdir(), 'clsi-thumb-'))
|
||||
const outputBase = Path.join(tmpDir, 'thumb')
|
||||
const jpegPath = outputBase + '.jpg'
|
||||
|
||||
try {
|
||||
await execFileAsync(
|
||||
'pdftocairo',
|
||||
[
|
||||
'-jpeg',
|
||||
'-jpegopt', 'quality=50',
|
||||
'-singlefile',
|
||||
'-scale-to-x', '190',
|
||||
'-scale-to-y', '-1',
|
||||
'-f', '1',
|
||||
'-l', '1',
|
||||
pdfPath,
|
||||
outputBase,
|
||||
],
|
||||
{ timeout: 30000 }
|
||||
)
|
||||
|
||||
const jpegStat = await fs.stat(jpegPath)
|
||||
res.setHeader('Content-Type', 'image/jpeg')
|
||||
res.setHeader('Content-Length', jpegStat.size)
|
||||
@@ -293,10 +306,11 @@ async function thumbnailFromBuild(req, res) {
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||
const readStream = fsSync.createReadStream(jpegPath)
|
||||
await pipeline(readStream, res)
|
||||
} catch (err) {
|
||||
logger.warn({ err, projectId, buildId }, 'thumbnail generation failed')
|
||||
if (!res.headersSent) res.sendStatus(500)
|
||||
} finally {
|
||||
await fs
|
||||
.rm(Path.dirname(jpegPath), { recursive: true, force: true })
|
||||
.catch(() => {})
|
||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user