ResourceWriter.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /* eslint-disable
  2. no-return-assign,
  3. no-unused-vars,
  4. no-useless-escape,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS101: Remove unnecessary use of Array.from
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. let ResourceWriter
  16. const { promisify } = require('node:util')
  17. const UrlCache = require('./UrlCache')
  18. const Path = require('node:path')
  19. const fs = require('node:fs')
  20. const async = require('async')
  21. const OutputFileFinder = require('./OutputFileFinder')
  22. const ResourceStateManager = require('./ResourceStateManager')
  23. const Metrics = require('@overleaf/metrics')
  24. const logger = require('@overleaf/logger')
  25. const settings = require('@overleaf/settings')
  26. const { shouldSkipMetrics } = require('./Metrics')
  27. const parallelFileDownloads = settings.parallelFileDownloads || 1
  28. module.exports = ResourceWriter = {
  29. syncResourcesToDisk(request, basePath, callback) {
  30. if (callback == null) {
  31. callback = function () {}
  32. }
  33. if (request.syncType === 'incremental') {
  34. logger.debug(
  35. { projectId: request.project_id, userId: request.user_id },
  36. 'incremental sync'
  37. )
  38. return ResourceStateManager.checkProjectStateMatches(
  39. request.syncState,
  40. basePath,
  41. function (error, resourceList) {
  42. if (error != null) {
  43. return callback(error)
  44. }
  45. return ResourceWriter._removeExtraneousFiles(
  46. request,
  47. resourceList,
  48. basePath,
  49. function (error, outputFiles, allFiles) {
  50. if (error != null) {
  51. return callback(error)
  52. }
  53. return ResourceStateManager.checkResourceFiles(
  54. resourceList,
  55. allFiles,
  56. basePath,
  57. function (error) {
  58. if (error != null) {
  59. return callback(error)
  60. }
  61. return ResourceWriter.saveIncrementalResourcesToDisk(
  62. request.project_id,
  63. request.resources,
  64. basePath,
  65. function (error) {
  66. if (error != null) {
  67. return callback(error)
  68. }
  69. return callback(null, resourceList)
  70. }
  71. )
  72. }
  73. )
  74. }
  75. )
  76. }
  77. )
  78. }
  79. logger.debug(
  80. { projectId: request.project_id, userId: request.user_id },
  81. 'full sync'
  82. )
  83. UrlCache.createProjectDir(request.project_id, error => {
  84. if (error != null) {
  85. return callback(error)
  86. }
  87. ResourceWriter.saveAllResourcesToDisk(
  88. request,
  89. basePath,
  90. function (error) {
  91. if (error != null) {
  92. return callback(error)
  93. }
  94. return ResourceStateManager.saveProjectState(
  95. request.syncState,
  96. request.resources,
  97. basePath,
  98. function (error) {
  99. if (error != null) {
  100. return callback(error)
  101. }
  102. return callback(null, request.resources)
  103. }
  104. )
  105. }
  106. )
  107. })
  108. },
  109. saveIncrementalResourcesToDisk(projectId, resources, basePath, callback) {
  110. if (callback == null) {
  111. callback = function () {}
  112. }
  113. return ResourceWriter._createDirectory(basePath, error => {
  114. if (error != null) {
  115. return callback(error)
  116. }
  117. const jobs = Array.from(resources).map(resource =>
  118. (resource => {
  119. return callback =>
  120. ResourceWriter._writeResourceToDisk(
  121. projectId,
  122. resource,
  123. basePath,
  124. callback
  125. )
  126. })(resource)
  127. )
  128. return async.parallelLimit(jobs, parallelFileDownloads, callback)
  129. })
  130. },
  131. saveAllResourcesToDisk(request, basePath, callback) {
  132. if (callback == null) {
  133. callback = function () {}
  134. }
  135. return ResourceWriter._createDirectory(basePath, error => {
  136. if (error != null) {
  137. return callback(error)
  138. }
  139. const { project_id: projectId, resources } = request
  140. ResourceWriter._removeExtraneousFiles(
  141. request,
  142. resources,
  143. basePath,
  144. error => {
  145. if (error != null) {
  146. return callback(error)
  147. }
  148. const jobs = Array.from(resources).map(resource =>
  149. (resource => {
  150. return callback =>
  151. ResourceWriter._writeResourceToDisk(
  152. projectId,
  153. resource,
  154. basePath,
  155. callback
  156. )
  157. })(resource)
  158. )
  159. return async.parallelLimit(jobs, parallelFileDownloads, callback)
  160. }
  161. )
  162. })
  163. },
  164. _createDirectory(basePath, callback) {
  165. if (callback == null) {
  166. callback = function () {}
  167. }
  168. return fs.mkdir(basePath, function (err) {
  169. if (err != null) {
  170. if (err.code === 'EEXIST') {
  171. return callback()
  172. } else {
  173. logger.debug({ err, dir: basePath }, 'error creating directory')
  174. return callback(err)
  175. }
  176. } else {
  177. return callback()
  178. }
  179. })
  180. },
  181. _removeExtraneousFiles(request, resources, basePath, _callback) {
  182. if (_callback == null) {
  183. _callback = function () {}
  184. }
  185. const timer = new Metrics.Timer(
  186. 'unlink-output-files',
  187. 1,
  188. request.metricsOpts
  189. )
  190. const callback = function (error, ...result) {
  191. if (!shouldSkipMetrics(request)) timer.done()
  192. return _callback(error, ...Array.from(result))
  193. }
  194. return OutputFileFinder.findOutputFiles(
  195. resources,
  196. basePath,
  197. (error, outputFiles, allFiles) => {
  198. if (error != null) {
  199. return callback(error)
  200. }
  201. const jobs = []
  202. for (const { path } of outputFiles || []) {
  203. const shouldDelete = ResourceWriter.isExtraneousFile(path)
  204. if (shouldDelete) {
  205. jobs.push(callback =>
  206. ResourceWriter._deleteFileIfNotDirectory(
  207. Path.join(basePath, path),
  208. callback
  209. )
  210. )
  211. }
  212. }
  213. return async.series(jobs, function (error) {
  214. if (error != null) {
  215. return callback(error)
  216. }
  217. return callback(null, outputFiles, allFiles)
  218. })
  219. }
  220. )
  221. },
  222. isExtraneousFile(path) {
  223. let shouldDelete = true
  224. if (
  225. path.match(/^output\./) ||
  226. path.match(/\.aux$/) ||
  227. path.match(/^cache\//)
  228. ) {
  229. // knitr cache
  230. shouldDelete = false
  231. }
  232. if (path.match(/^output-.*/)) {
  233. // Tikz cached figures (default case)
  234. shouldDelete = false
  235. }
  236. if (path.match(/\.(pdf|dpth|md5)$/)) {
  237. // Tikz cached figures (by extension)
  238. shouldDelete = false
  239. }
  240. if (
  241. path.match(/\.(pygtex|pygstyle)$/) ||
  242. path.match(/(^|\/)_minted-[^\/]+\//)
  243. ) {
  244. // minted files/directory
  245. shouldDelete = false
  246. }
  247. if (path.match(/\.md\.tex$/) || path.match(/(^|\/)_markdown_[^\/]+\//)) {
  248. // markdown files/directory
  249. shouldDelete = false
  250. }
  251. if (path.match(/-eps-converted-to\.pdf$/)) {
  252. // Epstopdf generated files
  253. shouldDelete = false
  254. }
  255. if (
  256. path === 'output.tar.gz' ||
  257. path === 'output.synctex.gz' ||
  258. path === 'output.pdfxref' ||
  259. path === 'output.pdf' ||
  260. path === 'output.dvi' ||
  261. path === 'output.log' ||
  262. path === 'output.xdv' ||
  263. path === 'output.stdout' ||
  264. path === 'output.stderr'
  265. ) {
  266. shouldDelete = true
  267. }
  268. if (path === 'output.tex') {
  269. // created by TikzManager if present in output files
  270. shouldDelete = true
  271. }
  272. return shouldDelete
  273. },
  274. _deleteFileIfNotDirectory(path, callback) {
  275. if (callback == null) {
  276. callback = function () {}
  277. }
  278. return fs.stat(path, function (error, stat) {
  279. if (error != null && error.code === 'ENOENT') {
  280. return callback()
  281. } else if (error != null) {
  282. logger.err(
  283. { err: error, path },
  284. 'error stating file in deleteFileIfNotDirectory'
  285. )
  286. return callback(error)
  287. } else if (stat.isFile()) {
  288. return fs.unlink(path, function (error) {
  289. if (error != null) {
  290. logger.err(
  291. { err: error, path },
  292. 'error removing file in deleteFileIfNotDirectory'
  293. )
  294. return callback(error)
  295. } else {
  296. return callback()
  297. }
  298. })
  299. } else {
  300. return callback()
  301. }
  302. })
  303. },
  304. _writeResourceToDisk(projectId, resource, basePath, callback) {
  305. if (callback == null) {
  306. callback = function () {}
  307. }
  308. return ResourceWriter.checkPath(
  309. basePath,
  310. resource.path,
  311. function (error, path) {
  312. if (error != null) {
  313. return callback(error)
  314. }
  315. return fs.mkdir(
  316. Path.dirname(path),
  317. { recursive: true },
  318. function (error) {
  319. if (error != null) {
  320. return callback(error)
  321. }
  322. // TODO: Don't overwrite file if it hasn't been modified
  323. if (resource.url != null) {
  324. return UrlCache.downloadUrlToFile(
  325. projectId,
  326. resource.url,
  327. resource.fallbackURL,
  328. path,
  329. resource.modified,
  330. function (err) {
  331. if (err != null) {
  332. logger.err(
  333. {
  334. err,
  335. projectId,
  336. path,
  337. resourceUrl: resource.url,
  338. modified: resource.modified,
  339. },
  340. 'error downloading file for resources'
  341. )
  342. Metrics.inc('download-failed')
  343. }
  344. return callback()
  345. }
  346. ) // try and continue compiling even if http resource can not be downloaded at this time
  347. } else {
  348. fs.writeFile(path, resource.content, callback)
  349. }
  350. }
  351. )
  352. }
  353. )
  354. },
  355. checkPath(basePath, resourcePath, callback) {
  356. const path = Path.normalize(Path.join(basePath, resourcePath))
  357. if (path.slice(0, basePath.length + 1) !== basePath + '/') {
  358. return callback(new Error('resource path is outside root directory'))
  359. } else {
  360. return callback(null, path)
  361. }
  362. },
  363. }
  364. module.exports.promises = {
  365. syncResourcesToDisk: promisify(ResourceWriter.syncResourcesToDisk),
  366. saveIncrementalResourcesToDisk: promisify(
  367. ResourceWriter.saveIncrementalResourcesToDisk
  368. ),
  369. saveAllResourcesToDisk: promisify(ResourceWriter.saveAllResourcesToDisk),
  370. checkPath: promisify(ResourceWriter.checkPath),
  371. }