Files
overleaf-cep/services/web/app/src/Features/Templates/TemplatesManager.mjs
T
Jakob Ackermann 7f5af99ee9 [web] determine rootResourcePath if not set in compile request (#34398)
* [web] determine rootResourcePath if not set in compile request

This affects the following compile requests:
- compile request from "Download PDF" button on project dashboard
  Pre-resolving the rootResourcePath for all projects is too expensive.
  Adding an endpoint to resolve it is too slow and doubles project auth
  check.
- custom frontends
  Can't do much about these. I tried blocking them, requests still come
  in after weeks of returning 400s.

The rootDoc maintenance methods now return {rootResourcePath,rootDocId}
rather than just the rootDocId. This avoids an extra project lookup when
the rootDocId was updated.

GitOrigin-RevId: ab7c539b7ffd162bde0903fe1040a48834a245f5
2026-06-09 08:07:36 +00:00

154 lines
4.6 KiB
JavaScript

import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
import ProjectOptionsHandlerModule from '../Project/ProjectOptionsHandler.mjs'
import ProjectRootDocManagerModule from '../Project/ProjectRootDocManager.mjs'
import ProjectUploadManager from '../Uploads/ProjectUploadManager.mjs'
import fs from 'node:fs'
import util from 'node:util'
import logger from '@overleaf/logger'
import {
fetchJson,
fetchStreamWithResponse,
RequestFailedError,
} from '@overleaf/fetch-utils'
import settings from '@overleaf/settings'
import crypto from 'node:crypto'
import Errors from '../Errors/Errors.js'
import { pipeline } from 'node:stream/promises'
import ClsiCacheManager from '../Compile/ClsiCacheManager.mjs'
import Path from 'node:path'
import OError from '@overleaf/o-error'
const { promises: ProjectRootDocManager } = ProjectRootDocManagerModule
const { promises: ProjectOptionsHandler } = ProjectOptionsHandlerModule
const TemplatesManager = {
async createProjectFromV1Template(
brandVariationId,
compiler,
mainFile,
templateId,
templateName,
templateVersionId,
userId,
imageName
) {
compiler = ProjectOptionsHandler.normalizeCompiler(
compiler || settings.defaultLatexCompiler
)
imageName = ProjectOptionsHandler.normalizeImageName(
imageName || 'wl_texlive:2018.1'
)
const zipUrl = `${settings.apis.v1.url}/api/v1/overleaf/templates/${templateVersionId}`
const zipReq = await fetchStreamWithResponse(zipUrl, {
basicAuth: {
user: settings.apis.v1.user,
password: settings.apis.v1.pass,
},
signal: AbortSignal.timeout(settings.apis.v1.timeout),
})
const projectName = ProjectDetailsHandler.fixProjectName(templateName)
const dumpPath = `${settings.path.dumpFolder}/${crypto.randomUUID()}_templates-manager`
const writeStream = fs.createWriteStream(dumpPath)
try {
const attributes = {
fromV1TemplateId: templateId,
fromV1TemplateVersionId: templateVersionId,
compiler,
imageName,
}
if (brandVariationId) attributes.brandVariationId = brandVariationId
await pipeline(zipReq.stream, writeStream)
if (zipReq.response.status !== 200) {
logger.warn(
{ uri: zipUrl, statusCode: zipReq.response.status },
'non-success code getting zip from template API'
)
throw new OError('get zip failed', { status: zipReq.response.status })
}
const { fileEntries, docEntries, project } =
await ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
userId,
projectName,
dumpPath,
attributes
)
const prepareClsiCacheInBackground = ClsiCacheManager.prepareClsiCache(
project._id,
userId,
{ templateVersionId, imageName: imageName && Path.basename(imageName) }
).catch(err => {
logger.warn(
{ err, templateVersionId, projectId: project._id },
'failed to prepare clsi-cache from template'
)
return undefined
})
await TemplatesManager._setMainFile(project, mainFile)
const found = await prepareClsiCacheInBackground
if (found === false && project.rootDoc_id) {
ClsiCacheManager.createTemplateClsiCache({
templateVersionId,
project,
fileEntries,
docEntries,
}).catch(err => {
logger.error(
{ err, templateVersionId },
'failed to create template clsi-cache'
)
})
}
return project
} finally {
await fs.promises.unlink(dumpPath)
}
},
async _setMainFile(project, mainFile) {
if (mainFile == null) {
return
}
const result = await ProjectRootDocManager.setRootDocFromName(
project._id,
mainFile
)
if (result) project.rootDoc_id = result.rootDocId
},
async fetchFromV1(templateId) {
const url = new URL(`/api/v2/templates/${templateId}`, settings.apis.v1.url)
try {
return await fetchJson(url, {
basicAuth: {
user: settings.apis.v1.user,
password: settings.apis.v1.pass,
},
signal: AbortSignal.timeout(settings.apis.v1.timeout),
})
} catch (err) {
if (err instanceof RequestFailedError && err.response.status === 404) {
throw new Errors.NotFoundError()
} else {
throw err
}
}
},
}
export default {
promises: TemplatesManager,
createProjectFromV1Template: util.callbackify(
TemplatesManager.createProjectFromV1Template
),
fetchFromV1: util.callbackify(TemplatesManager.fetchFromV1),
}