Просмотр исходного кода

Merge pull request #28264 from overleaf/jpa-synctex

[web] use standard request handling for SyncTeX requests

GitOrigin-RevId: ad5ba1834241d5939675f2533940ade741fc5abf
Jakob Ackermann 11 месяцев назад
Родитель
Сommit
03320bb377

+ 55 - 3
services/web/app/src/Features/Compile/ClsiManager.js

@@ -818,9 +818,9 @@ function _finaliseRequest(projectId, options, project, docs, files) {
   }
 }
 
-async function wordCount(projectId, userId, file, options, clsiserverid) {
-  const { compileBackendClass, compileGroup } = options
-  const req = await _buildRequest(projectId, options)
+async function wordCount(projectId, userId, file, limits, clsiserverid) {
+  const { compileBackendClass, compileGroup } = limits
+  const req = await _buildRequest(projectId, limits)
   const filename = file || req.compile.rootResourcePath
   const url = _getCompilerUrl(
     compileBackendClass,
@@ -847,6 +847,56 @@ async function wordCount(projectId, userId, file, options, clsiserverid) {
   return body
 }
 
+async function syncTeX(
+  projectId,
+  userId,
+  {
+    direction,
+    compileFromClsiCache,
+    limits,
+    imageName,
+    validatedOptions,
+    clsiServerId,
+  }
+) {
+  const { compileBackendClass, compileGroup } = limits
+  const url = _getCompilerUrl(
+    compileBackendClass,
+    compileGroup,
+    projectId,
+    userId,
+    `sync/${direction}`
+  )
+  url.searchParams.set(
+    'compileFromClsiCache',
+    compileFromClsiCache && ['alpha', 'priority'].includes(compileGroup)
+  )
+  url.searchParams.set('imageName', imageName)
+  for (const [key, value] of Object.entries(validatedOptions)) {
+    url.searchParams.set(key, value)
+  }
+  const opts = {
+    method: 'GET',
+  }
+  try {
+    const { body } = await _makeRequestWithClsiServerId(
+      projectId,
+      userId,
+      compileGroup,
+      compileBackendClass,
+      url,
+      opts,
+      clsiServerId
+    )
+    return body
+  } catch (err) {
+    if (err instanceof RequestFailedError && err.response.status === 404) {
+      throw new Errors.NotFoundError()
+    }
+    throw err
+  }
+}
+
 function _getClsiServerIdFromResponse(response) {
   const setCookieHeaders = response.headers.raw()['set-cookie'] ?? []
   for (const header of setCookieHeaders) {
@@ -883,6 +933,7 @@ module.exports = {
   deleteAuxFiles: callbackify(deleteAuxFiles),
   getOutputFileStream: callbackify(getOutputFileStream),
   wordCount: callbackify(wordCount),
+  syncTeX: callbackify(syncTeX),
   promises: {
     sendRequest,
     sendExternalRequest,
@@ -890,5 +941,6 @@ module.exports = {
     deleteAuxFiles,
     getOutputFileStream,
     wordCount,
+    syncTeX,
   },
 }

+ 31 - 69
services/web/app/src/Features/Compile/CompileController.js

@@ -8,6 +8,7 @@ const CompileManager = require('./CompileManager')
 const ClsiManager = require('./ClsiManager')
 const logger = require('@overleaf/logger')
 const Settings = require('@overleaf/settings')
+const Errors = require('../Errors/Errors')
 const SessionManager = require('../Authentication/SessionManager')
 const { RateLimiter } = require('../../infrastructure/RateLimiter')
 const ClsiCookieManager = require('./ClsiCookieManager')(
@@ -38,16 +39,6 @@ function getOutputFilesArchiveSpecification(projectId, userId, buildId) {
   }
 }
 
-async function getImageNameForProject(projectId) {
-  const project = await ProjectGetter.promises.getProject(projectId, {
-    imageName: 1,
-  })
-  if (!project) {
-    throw new Error('project not found')
-  }
-  return project.imageName
-}
-
 async function getPdfCachingMinChunkSize(req, res) {
   const { variant } = await SplitTestHandler.promises.getAssignment(
     req,
@@ -121,6 +112,32 @@ async function _getSplitTestOptions(req, res) {
   }
 }
 
+async function _syncTeX(req, res, direction, validatedOptions) {
+  const projectId = req.params.Project_id
+  const { editorId, buildId, clsiserverid: clsiServerId } = req.query
+  if (!editorId?.match(/^[a-f0-9-]+$/)) throw new Error('invalid ?editorId')
+  if (!buildId?.match(/^[a-f0-9-]+$/)) throw new Error('invalid ?buildId')
+
+  const userId = CompileController._getUserIdForCompile(req)
+  const { compileFromClsiCache } = await _getSplitTestOptions(req, res)
+  try {
+    const body = await CompileManager.promises.syncTeX(projectId, userId, {
+      direction,
+      compileFromClsiCache,
+      validatedOptions: {
+        ...validatedOptions,
+        editorId,
+        buildId,
+      },
+      clsiServerId,
+    })
+    res.json(body)
+  } catch (err) {
+    if (err instanceof Errors.NotFoundError) return res.status(404).end()
+    throw err
+  }
+}
+
 const _CompileController = {
   async compile(req, res) {
     res.setTimeout(COMPILE_TIMEOUT_MS)
@@ -470,18 +487,8 @@ const _CompileController = {
     return url
   },
 
-  // compute a POST url for a project, user (optional) and action
-  _getUrl(projectId, userId, action) {
-    let path = `/project/${projectId}`
-    if (userId != null) {
-      path += `/user/${userId}`
-    }
-    return `${path}/${action}`
-  },
-
   async proxySyncPdf(req, res) {
-    const projectId = req.params.Project_id
-    const { page, h, v, editorId, buildId } = req.query
+    const { page, h, v } = req.query
     if (!page?.match(/^\d+$/)) {
       throw new Error('invalid page parameter')
     }
@@ -491,28 +498,11 @@ const _CompileController = {
     if (!v?.match(/^-?\d+\.\d+$/)) {
       throw new Error('invalid v parameter')
     }
-    // whether this request is going to a per-user container
-    const userId = CompileController._getUserIdForCompile(req)
-
-    const imageName = await getImageNameForProject(projectId)
-
-    const { compileFromClsiCache } = await _getSplitTestOptions(req, res)
-
-    const url = _CompileController._getUrl(projectId, userId, 'sync/pdf')
-
-    await CompileController._proxyToClsi(
-      projectId,
-      'sync-to-pdf',
-      url,
-      { page, h, v, imageName, editorId, buildId, compileFromClsiCache },
-      req,
-      res
-    )
+    await _syncTeX(req, res, 'pdf', { page, h, v })
   },
 
   async proxySyncCode(req, res) {
-    const projectId = req.params.Project_id
-    const { file, line, column, editorId, buildId } = req.query
+    const { file, line, column } = req.query
     if (file == null) {
       throw new Error('missing file parameter')
     }
@@ -531,40 +521,12 @@ const _CompileController = {
     if (!column?.match(/^\d+$/)) {
       throw new Error('invalid column parameter')
     }
-    const userId = CompileController._getUserIdForCompile(req)
-
-    const imageName = await getImageNameForProject(projectId)
-
-    const { compileFromClsiCache } = await _getSplitTestOptions(req, res)
-
-    const url = _CompileController._getUrl(projectId, userId, 'sync/code')
-    await CompileController._proxyToClsi(
-      projectId,
-      'sync-to-code',
-      url,
-      {
-        file,
-        line,
-        column,
-        imageName,
-        editorId,
-        buildId,
-        compileFromClsiCache,
-      },
-      req,
-      res
-    )
+    await _syncTeX(req, res, 'code', { file, line, column })
   },
 
   async _proxyToClsi(projectId, action, url, qs, req, res) {
     const limits =
       await CompileManager.promises.getProjectCompileLimits(projectId)
-    if (
-      qs?.compileFromClsiCache &&
-      !['alpha', 'priority'].includes(limits.compileGroup)
-    ) {
-      qs.compileFromClsiCache = false
-    }
     return CompileController._proxyToClsiWithLimits(
       projectId,
       action,

+ 29 - 0
services/web/app/src/Features/Compile/CompileManager.js

@@ -110,7 +110,13 @@ async function getProjectCompileLimits(projectId) {
   const project = await ProjectGetter.promises.getProject(projectId, {
     owner_ref: 1,
   })
+  return _getProjectCompileLimits(project)
+}
 
+async function _getProjectCompileLimits(project) {
+  if (!project) {
+    throw new Error('project not found')
+  }
   const owner = await UserGetter.promises.getUser(project.owner_ref, {
     _id: 1,
     alphaProgram: 1,
@@ -162,6 +168,27 @@ async function wordCount(projectId, userId, file, clsiserverid) {
   )
 }
 
+async function syncTeX(
+  projectId,
+  userId,
+  { direction, compileFromClsiCache, validatedOptions, clsiServerId }
+) {
+  const project = await ProjectGetter.promises.getProject(projectId, {
+    owner_ref: 1,
+    imageName: 1,
+  })
+  const limits = await _getProjectCompileLimits(project)
+  const { imageName } = project
+  return await ClsiManager.promises.syncTeX(projectId, userId, {
+    direction,
+    limits,
+    imageName,
+    compileFromClsiCache,
+    validatedOptions,
+    clsiServerId,
+  })
+}
+
 async function stopCompile(projectId, userId) {
   const limits =
     await CompileManager.promises.getProjectCompileLimits(projectId)
@@ -188,6 +215,7 @@ module.exports = CompileManager = {
     getProjectCompileLimits,
     stopCompile,
     wordCount,
+    syncTeX,
   },
   compile: callbackifyMultiResult(instrumentedCompile, [
     'status',
@@ -249,6 +277,7 @@ module.exports = CompileManager = {
   },
 
   wordCount: callbackify(wordCount),
+  syncTeX: callbackify(syncTeX),
 }
 
 const autoCompileRateLimiters = new Map()

+ 47 - 30
services/web/test/unit/src/Compile/CompileControllerTests.js

@@ -22,6 +22,7 @@ describe('CompileController', function () {
       promises: {
         compile: sinon.stub(),
         getProjectCompileLimits: sinon.stub(),
+        syncTeX: sinon.stub(),
       },
     }
     this.ClsiManager = {
@@ -594,16 +595,24 @@ describe('CompileController', function () {
     })
   })
   describe('proxySyncCode', function () {
-    let file, line, column, imageName, editorId, buildId
+    let file, line, column, imageName, editorId, buildId, clsiServerId
 
     beforeEach(async function () {
       this.req.params = { Project_id: this.projectId }
+      clsiServerId = 'clsi-1'
       file = 'main.tex'
       line = String(Date.now())
       column = String(Date.now() + 1)
       editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
       buildId = '195b4a3f9e7-03e5be430a9e7796'
-      this.req.query = { file, line, column, editorId, buildId }
+      this.req.query = {
+        file,
+        line,
+        column,
+        editorId,
+        buildId,
+        clsiserverid: clsiServerId,
+      }
 
       imageName = 'foo/bar:tag-0'
       this.ProjectGetter.promises.getProject = sinon
@@ -615,37 +624,45 @@ describe('CompileController', function () {
       await this.CompileController.proxySyncCode(this.req, this.res, this.next)
     })
 
-    it('should proxy the request with an imageName', function () {
-      expect(this.CompileController._proxyToClsi).to.have.been.calledWith(
+    it('should parse the parameters', function () {
+      expect(this.CompileManager.promises.syncTeX).to.have.been.calledWith(
         this.projectId,
-        'sync-to-code',
-        `/project/${this.projectId}/user/${this.user_id}/sync/code`,
+        this.user_id,
         {
-          file,
-          line,
-          column,
-          imageName,
-          editorId,
-          buildId,
+          direction: 'code',
           compileFromClsiCache: false,
-        },
-        this.req,
-        this.res
+          validatedOptions: {
+            file,
+            line,
+            column,
+            editorId,
+            buildId,
+          },
+          clsiServerId,
+        }
       )
     })
   })
 
   describe('proxySyncPdf', function () {
-    let page, h, v, imageName, editorId, buildId
+    let page, h, v, imageName, editorId, buildId, clsiServerId
 
     beforeEach(async function () {
       this.req.params = { Project_id: this.projectId }
+      clsiServerId = 'clsi-1'
       page = String(Date.now())
       h = String(Math.random())
       v = String(Math.random())
       editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
       buildId = '195b4a3f9e7-03e5be430a9e7796'
-      this.req.query = { page, h, v, editorId, buildId }
+      this.req.query = {
+        page,
+        h,
+        v,
+        editorId,
+        buildId,
+        clsiserverid: clsiServerId,
+      }
 
       imageName = 'foo/bar:tag-1'
       this.ProjectGetter.promises.getProject = sinon
@@ -657,22 +674,22 @@ describe('CompileController', function () {
       await this.CompileController.proxySyncPdf(this.req, this.res, this.next)
     })
 
-    it('should proxy the request with an imageName', function () {
-      expect(this.CompileController._proxyToClsi).to.have.been.calledWith(
+    it('should parse the parameters', function () {
+      expect(this.CompileManager.promises.syncTeX).to.have.been.calledWith(
         this.projectId,
-        'sync-to-pdf',
-        `/project/${this.projectId}/user/${this.user_id}/sync/pdf`,
+        this.user_id,
         {
-          page,
-          h,
-          v,
-          imageName,
-          editorId,
-          buildId,
+          direction: 'pdf',
           compileFromClsiCache: false,
-        },
-        this.req,
-        this.res
+          validatedOptions: {
+            page,
+            h,
+            v,
+            editorId,
+            buildId,
+          },
+          clsiServerId,
+        }
       )
     })
   })