Client.js 6.3 KB

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