ResourceWriter.js 11 KB

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