ResourceWriter.js 10 KB

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