ResourceWriter.js 11 KB

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