ResourceWriter.js 11 KB

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