Client.js 5.4 KB

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