ResourceWriter.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. /* eslint-disable
  2. camelcase,
  3. no-return-assign,
  4. no-unused-vars,
  5. no-useless-escape,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS101: Remove unnecessary use of Array.from
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. let ResourceWriter
  17. const { promisify } = require('util')
  18. const UrlCache = require('./UrlCache')
  19. const Path = require('path')
  20. const fs = require('fs')
  21. const async = require('async')
  22. const OutputFileFinder = require('./OutputFileFinder')
  23. const ResourceStateManager = require('./ResourceStateManager')
  24. const Metrics = require('./Metrics')
  25. const logger = require('@overleaf/logger')
  26. const settings = require('@overleaf/settings')
  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. { project_id: request.project_id, user_id: 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. { project_id: request.project_id, user_id: 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(project_id, 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. project_id,
  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, 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. project_id,
  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. timer.done()
  192. return _callback(error, ...Array.from(result))
  193. }
  194. return OutputFileFinder.findOutputFiles(
  195. resources,
  196. basePath,
  197. function (error, outputFiles, allFiles) {
  198. if (error != null) {
  199. return callback(error)
  200. }
  201. const jobs = []
  202. for (const file of Array.from(outputFiles || [])) {
  203. ;(function (file) {
  204. const { path } = file
  205. let should_delete = true
  206. if (
  207. path.match(/^output\./) ||
  208. path.match(/\.aux$/) ||
  209. path.match(/^cache\//)
  210. ) {
  211. // knitr cache
  212. should_delete = false
  213. }
  214. if (path.match(/^output-.*/)) {
  215. // Tikz cached figures (default case)
  216. should_delete = false
  217. }
  218. if (path.match(/\.(pdf|dpth|md5)$/)) {
  219. // Tikz cached figures (by extension)
  220. should_delete = false
  221. }
  222. if (
  223. path.match(/\.(pygtex|pygstyle)$/) ||
  224. path.match(/(^|\/)_minted-[^\/]+\//)
  225. ) {
  226. // minted files/directory
  227. should_delete = false
  228. }
  229. if (
  230. path.match(/\.md\.tex$/) ||
  231. path.match(/(^|\/)_markdown_[^\/]+\//)
  232. ) {
  233. // markdown files/directory
  234. should_delete = false
  235. }
  236. if (path.match(/-eps-converted-to\.pdf$/)) {
  237. // Epstopdf generated files
  238. should_delete = false
  239. }
  240. if (
  241. path === 'output.pdf' ||
  242. path === 'output.dvi' ||
  243. path === 'output.log' ||
  244. path === 'output.xdv' ||
  245. path === 'output.stdout' ||
  246. path === 'output.stderr'
  247. ) {
  248. should_delete = true
  249. }
  250. if (path === 'output.tex') {
  251. // created by TikzManager if present in output files
  252. should_delete = true
  253. }
  254. if (should_delete) {
  255. return jobs.push(callback =>
  256. ResourceWriter._deleteFileIfNotDirectory(
  257. Path.join(basePath, path),
  258. callback
  259. )
  260. )
  261. }
  262. })(file)
  263. }
  264. return async.series(jobs, function (error) {
  265. if (error != null) {
  266. return callback(error)
  267. }
  268. return callback(null, outputFiles, allFiles)
  269. })
  270. }
  271. )
  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(project_id, 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. project_id,
  325. resource.url,
  326. path,
  327. resource.modified,
  328. function (err) {
  329. if (err != null) {
  330. logger.err(
  331. {
  332. err,
  333. project_id,
  334. path,
  335. resource_url: resource.url,
  336. modified: resource.modified,
  337. },
  338. 'error downloading file for resources'
  339. )
  340. Metrics.inc('download-failed')
  341. }
  342. return callback()
  343. }
  344. ) // try and continue compiling even if http resource can not be downloaded at this time
  345. } else {
  346. fs.writeFile(path, resource.content, callback)
  347. }
  348. }
  349. )
  350. }
  351. )
  352. },
  353. checkPath(basePath, resourcePath, callback) {
  354. const path = Path.normalize(Path.join(basePath, resourcePath))
  355. if (path.slice(0, basePath.length + 1) !== basePath + '/') {
  356. return callback(new Error('resource path is outside root directory'))
  357. } else {
  358. return callback(null, path)
  359. }
  360. },
  361. }
  362. module.exports.promises = {
  363. syncResourcesToDisk: promisify(ResourceWriter.syncResourcesToDisk),
  364. saveIncrementalResourcesToDisk: promisify(
  365. ResourceWriter.saveIncrementalResourcesToDisk
  366. ),
  367. saveAllResourcesToDisk: promisify(ResourceWriter.saveAllResourcesToDisk),
  368. checkPath: promisify(ResourceWriter.checkPath),
  369. }