Client.js 6.6 KB

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