Client.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. const express = require('express')
  2. const { fetchJson, fetchNothing } = require('@overleaf/fetch-utils')
  3. const fs = require('node:fs')
  4. const fsPromises = require('node:fs/promises')
  5. const Settings = require('@overleaf/settings')
  6. const host = Settings.apis.clsi.url
  7. function randomId() {
  8. return Math.random().toString(16).slice(2)
  9. }
  10. function compile(projectId, data) {
  11. if (data) {
  12. // Enable pdf caching unless disabled explicitly.
  13. data.options = Object.assign({}, { enablePdfCaching: true }, data.options)
  14. }
  15. return fetchJson(`${host}/project/${projectId}/compile`, {
  16. method: 'POST',
  17. json: {
  18. compile: data,
  19. },
  20. })
  21. }
  22. async function stopCompile(projectId) {
  23. return await fetchNothing(`${host}/project/${projectId}/compile/stop`, {
  24. method: 'POST',
  25. })
  26. }
  27. async function clearCache(projectId) {
  28. await fetchNothing(`${host}/project/${projectId}`, {
  29. method: 'DELETE',
  30. })
  31. }
  32. function getOutputFile(response, type) {
  33. for (const file of response.compile.outputFiles) {
  34. if (file.type === type && file.url.match(`output.${type}`)) {
  35. return file
  36. }
  37. }
  38. return null
  39. }
  40. function runFakeFilestoreService(directory) {
  41. const app = express()
  42. app.use(express.static(directory))
  43. this.startFakeFilestoreApp(app)
  44. }
  45. function startFakeFilestoreApp(app) {
  46. let server
  47. before(function (done) {
  48. server = app.listen(error => {
  49. if (error) {
  50. done(new Error('error starting server: ' + error.message))
  51. } else {
  52. const addr = server.address()
  53. Settings.filestoreDomainOveride = `http://127.0.0.1:${addr.port}`
  54. done()
  55. }
  56. })
  57. })
  58. after(function (done) {
  59. server.close(done)
  60. })
  61. }
  62. function syncFromCode(projectId, file, line, column) {
  63. return syncFromCodeWithImage(projectId, file, line, column, '')
  64. }
  65. async function syncFromCodeWithImage(projectId, file, line, column, imageName) {
  66. const url = new URL(`${host}/project/${projectId}/sync/code`)
  67. url.searchParams.append('imageName', imageName)
  68. url.searchParams.append('file', file)
  69. url.searchParams.append('line', line)
  70. url.searchParams.append('column', column)
  71. return await fetchJson(url)
  72. }
  73. function syncFromPdf(projectId, page, h, v) {
  74. return syncFromPdfWithImage(projectId, page, h, v, '')
  75. }
  76. function syncFromPdfWithImage(projectId, page, h, v, imageName) {
  77. const url = new URL(`${host}/project/${projectId}/sync/pdf`)
  78. url.searchParams.append('imageName', imageName)
  79. url.searchParams.append('page', page)
  80. url.searchParams.append('h', h)
  81. url.searchParams.append('v', v)
  82. return fetchJson(url)
  83. }
  84. function wordcount(projectId, file) {
  85. const image = undefined
  86. return wordcountWithImage(projectId, file, image)
  87. }
  88. async function wordcountWithImage(projectId, file, image) {
  89. const url = new URL(`${host}/project/${projectId}/wordcount`)
  90. if (image) {
  91. url.searchParams.append('image', image)
  92. }
  93. url.searchParams.append('file', file)
  94. return await fetchJson(url)
  95. }
  96. async function compileDirectory(projectId, baseDirectory, directory) {
  97. const resources = []
  98. let entities = fs.readdirSync(`${baseDirectory}/${directory}`)
  99. let rootResourcePath = 'main.tex'
  100. while (entities.length > 0) {
  101. const entity = entities.pop()
  102. const stat = fs.statSync(`${baseDirectory}/${directory}/${entity}`)
  103. if (stat.isDirectory()) {
  104. entities = entities.concat(
  105. fs
  106. .readdirSync(`${baseDirectory}/${directory}/${entity}`)
  107. .map(subEntity => {
  108. if (subEntity === 'main.tex') {
  109. rootResourcePath = `${entity}/${subEntity}`
  110. }
  111. return `${entity}/${subEntity}`
  112. })
  113. )
  114. } else if (stat.isFile() && entity !== 'output.pdf') {
  115. const extension = entity.split('.').pop()
  116. if (
  117. [
  118. 'tex',
  119. 'bib',
  120. 'cls',
  121. 'sty',
  122. 'pdf_tex',
  123. 'Rtex',
  124. 'ist',
  125. 'md',
  126. 'Rmd',
  127. 'Rnw',
  128. ].indexOf(extension) > -1
  129. ) {
  130. resources.push({
  131. path: entity,
  132. content: fs
  133. .readFileSync(`${baseDirectory}/${directory}/${entity}`)
  134. .toString(),
  135. })
  136. } else if (
  137. ['eps', 'ttf', 'png', 'jpg', 'pdf', 'jpeg'].indexOf(extension) > -1
  138. ) {
  139. resources.push({
  140. path: entity,
  141. url: `http://filestore/${directory}/${entity}`,
  142. modified: stat.mtime,
  143. })
  144. }
  145. }
  146. }
  147. const req = {
  148. resources,
  149. rootResourcePath,
  150. }
  151. try {
  152. const options = await fsPromises.readFile(
  153. `${baseDirectory}/${directory}/options.json`
  154. )
  155. req.options = JSON.parse(options)
  156. } catch (error) {
  157. // noop
  158. }
  159. return await compile(projectId, req)
  160. }
  161. module.exports = {
  162. randomId,
  163. compile,
  164. stopCompile,
  165. clearCache,
  166. getOutputFile,
  167. runFakeFilestoreService,
  168. startFakeFilestoreApp,
  169. syncFromCode,
  170. syncFromCodeWithImage,
  171. syncFromPdf,
  172. syncFromPdfWithImage,
  173. compileDirectory,
  174. wordcount,
  175. wordcountWithImage,
  176. }