Client.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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 convertProjectToDocument(
  38. projectId,
  39. userId,
  40. type,
  41. request,
  42. responseFormat
  43. ) {
  44. const url = new URL(
  45. `${host}/project/${projectId}/user/${userId}/download/project-to-document`
  46. )
  47. url.searchParams.set('type', type)
  48. if (responseFormat) {
  49. url.searchParams.set('responseFormat', responseFormat)
  50. }
  51. const opts = { method: 'POST', json: { compile: request } }
  52. if (responseFormat === 'json') {
  53. return await fetchJson(url.href, opts)
  54. }
  55. return await fetchStream(url.href, opts)
  56. }
  57. async function stopCompile(projectId) {
  58. return await fetchNothing(`${host}/project/${projectId}/compile/stop`, {
  59. method: 'POST',
  60. })
  61. }
  62. async function clearCache(projectId) {
  63. return await fetchNothing(`${host}/project/${projectId}`, {
  64. method: 'DELETE',
  65. })
  66. }
  67. function getOutputFile(response, type) {
  68. for (const file of response.compile.outputFiles) {
  69. if (file.type === type && file.url.match(`output.${type}`)) {
  70. return file
  71. }
  72. }
  73. return null
  74. }
  75. function runFakeFilestoreService(directory) {
  76. const app = express()
  77. app.use(express.static(directory))
  78. this.startFakeFilestoreApp(app)
  79. }
  80. function startFakeFilestoreApp(app) {
  81. let server
  82. before(function (done) {
  83. server = app.listen(error => {
  84. if (error) {
  85. done(new Error('error starting server: ' + error.message))
  86. } else {
  87. const addr = server.address()
  88. Settings.filestoreDomainOveride = `http://127.0.0.1:${addr.port}`
  89. done()
  90. }
  91. })
  92. })
  93. after(function (done) {
  94. server.close(done)
  95. })
  96. }
  97. function syncFromCode(projectId, file, line, column) {
  98. return syncFromCodeWithImage(projectId, file, line, column, '')
  99. }
  100. async function syncFromCodeWithImage(projectId, file, line, column, imageName) {
  101. const url = new URL(`${host}/project/${projectId}/sync/code`)
  102. url.searchParams.append('imageName', imageName)
  103. url.searchParams.append('file', file)
  104. url.searchParams.append('line', line)
  105. url.searchParams.append('column', column)
  106. return await fetchJson(url)
  107. }
  108. function syncFromPdf(projectId, page, h, v) {
  109. return syncFromPdfWithImage(projectId, page, h, v, '')
  110. }
  111. function syncFromPdfWithImage(projectId, page, h, v, imageName) {
  112. const url = new URL(`${host}/project/${projectId}/sync/pdf`)
  113. url.searchParams.append('imageName', imageName)
  114. url.searchParams.append('page', page)
  115. url.searchParams.append('h', h)
  116. url.searchParams.append('v', v)
  117. return fetchJson(url)
  118. }
  119. function wordcount(projectId, file) {
  120. const image = undefined
  121. return wordcountWithImage(projectId, file, image)
  122. }
  123. async function wordcountWithImage(projectId, file, image) {
  124. const url = new URL(`${host}/project/${projectId}/wordcount`)
  125. if (image) {
  126. url.searchParams.append('image', image)
  127. }
  128. url.searchParams.append('file', file)
  129. return await fetchJson(url)
  130. }
  131. async function compileDirectory(projectId, baseDirectory, directory) {
  132. const resources = []
  133. let entities = fs.readdirSync(`${baseDirectory}/${directory}`)
  134. let rootResourcePath = 'main.tex'
  135. while (entities.length > 0) {
  136. const entity = entities.pop()
  137. const stat = fs.statSync(`${baseDirectory}/${directory}/${entity}`)
  138. if (stat.isDirectory()) {
  139. entities = entities.concat(
  140. fs
  141. .readdirSync(`${baseDirectory}/${directory}/${entity}`)
  142. .map(subEntity => {
  143. if (subEntity === 'main.tex') {
  144. rootResourcePath = `${entity}/${subEntity}`
  145. }
  146. return `${entity}/${subEntity}`
  147. })
  148. )
  149. } else if (stat.isFile() && entity !== 'output.pdf') {
  150. const extension = entity.split('.').pop()
  151. if (
  152. [
  153. 'tex',
  154. 'bib',
  155. 'cls',
  156. 'sty',
  157. 'pdf_tex',
  158. 'Rtex',
  159. 'ist',
  160. 'md',
  161. 'Rmd',
  162. 'Rnw',
  163. ].indexOf(extension) > -1
  164. ) {
  165. resources.push({
  166. path: entity,
  167. content: fs
  168. .readFileSync(`${baseDirectory}/${directory}/${entity}`)
  169. .toString(),
  170. })
  171. } else if (
  172. ['eps', 'ttf', 'png', 'jpg', 'pdf', 'jpeg'].indexOf(extension) > -1
  173. ) {
  174. resources.push({
  175. path: entity,
  176. url: `http://filestore/${directory}/${entity}`,
  177. modified: stat.mtime,
  178. })
  179. }
  180. }
  181. }
  182. const req = {
  183. resources,
  184. rootResourcePath,
  185. }
  186. try {
  187. const options = await fsPromises.readFile(
  188. `${baseDirectory}/${directory}/options.json`
  189. )
  190. req.options = JSON.parse(options)
  191. } catch (error) {
  192. // noop
  193. }
  194. return await compile(projectId, req)
  195. }
  196. function smokeTest() {
  197. return fetchString(`${host}/smoke_test_force`, {
  198. method: 'GET',
  199. })
  200. }
  201. export default {
  202. randomId,
  203. compile,
  204. convertProjectToDocument,
  205. convertDocument,
  206. stopCompile,
  207. clearCache,
  208. getOutputFile,
  209. smokeTest,
  210. runFakeFilestoreService,
  211. startFakeFilestoreApp,
  212. syncFromCode,
  213. syncFromCodeWithImage,
  214. syncFromPdf,
  215. syncFromPdfWithImage,
  216. compileDirectory,
  217. wordcount,
  218. wordcountWithImage,
  219. }