ResourceWriter.js 10 KB

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