ResourceWriter.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-return-assign,
  5. no-unused-vars,
  6. no-useless-escape,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS101: Remove unnecessary use of Array.from
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * DS207: Consider shorter variations of null checks
  15. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  16. */
  17. let ResourceWriter
  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('logger-sharelatex')
  26. const settings = require('settings-sharelatex')
  27. const parallelFileDownloads = settings.parallelFileDownloads || 1
  28. module.exports = ResourceWriter = {
  29. syncResourcesToDisk(request, basePath, callback) {
  30. if (callback == null) {
  31. callback = function(error, resourceList) {}
  32. }
  33. if (request.syncType === 'incremental') {
  34. logger.log(
  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. 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. } else {
  78. logger.log(
  79. { project_id: request.project_id, user_id: request.user_id },
  80. 'full sync'
  81. )
  82. return this.saveAllResourcesToDisk(
  83. request.project_id,
  84. request.resources,
  85. basePath,
  86. 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. },
  105. saveIncrementalResourcesToDisk(project_id, resources, basePath, callback) {
  106. if (callback == null) {
  107. callback = function(error) {}
  108. }
  109. return this._createDirectory(basePath, error => {
  110. if (error != null) {
  111. return callback(error)
  112. }
  113. const jobs = Array.from(resources).map(resource =>
  114. (resource => {
  115. return callback =>
  116. this._writeResourceToDisk(project_id, resource, basePath, callback)
  117. })(resource)
  118. )
  119. return async.parallelLimit(jobs, parallelFileDownloads, callback)
  120. })
  121. },
  122. saveAllResourcesToDisk(project_id, resources, basePath, callback) {
  123. if (callback == null) {
  124. callback = function(error) {}
  125. }
  126. return this._createDirectory(basePath, error => {
  127. if (error != null) {
  128. return callback(error)
  129. }
  130. return this._removeExtraneousFiles(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(error) {}
  152. }
  153. return fs.mkdir(basePath, function(err) {
  154. if (err != null) {
  155. if (err.code === 'EEXIST') {
  156. return callback()
  157. } else {
  158. logger.log({ err, dir: basePath }, 'error creating directory')
  159. return callback(err)
  160. }
  161. } else {
  162. return callback()
  163. }
  164. })
  165. },
  166. _removeExtraneousFiles(resources, basePath, _callback) {
  167. if (_callback == null) {
  168. _callback = function(error, outputFiles, allFiles) {}
  169. }
  170. const timer = new Metrics.Timer('unlink-output-files')
  171. const callback = function(error, ...result) {
  172. timer.done()
  173. return _callback(error, ...Array.from(result))
  174. }
  175. return OutputFileFinder.findOutputFiles(resources, basePath, function(
  176. error,
  177. outputFiles,
  178. allFiles
  179. ) {
  180. if (error != null) {
  181. return callback(error)
  182. }
  183. const jobs = []
  184. for (const file of Array.from(outputFiles || [])) {
  185. ;(function(file) {
  186. const { path } = file
  187. let should_delete = true
  188. if (
  189. path.match(/^output\./) ||
  190. path.match(/\.aux$/) ||
  191. path.match(/^cache\//)
  192. ) {
  193. // knitr cache
  194. should_delete = false
  195. }
  196. if (path.match(/^output-.*/)) {
  197. // Tikz cached figures (default case)
  198. should_delete = false
  199. }
  200. if (path.match(/\.(pdf|dpth|md5)$/)) {
  201. // Tikz cached figures (by extension)
  202. should_delete = false
  203. }
  204. if (
  205. path.match(/\.(pygtex|pygstyle)$/) ||
  206. path.match(/(^|\/)_minted-[^\/]+\//)
  207. ) {
  208. // minted files/directory
  209. should_delete = false
  210. }
  211. if (
  212. path.match(/\.md\.tex$/) ||
  213. path.match(/(^|\/)_markdown_[^\/]+\//)
  214. ) {
  215. // markdown files/directory
  216. should_delete = false
  217. }
  218. if (path.match(/-eps-converted-to\.pdf$/)) {
  219. // Epstopdf generated files
  220. should_delete = false
  221. }
  222. if (
  223. path === 'output.pdf' ||
  224. path === 'output.dvi' ||
  225. path === 'output.log' ||
  226. path === 'output.xdv'
  227. ) {
  228. should_delete = true
  229. }
  230. if (path === 'output.tex') {
  231. // created by TikzManager if present in output files
  232. should_delete = true
  233. }
  234. if (should_delete) {
  235. return jobs.push(callback =>
  236. ResourceWriter._deleteFileIfNotDirectory(
  237. Path.join(basePath, path),
  238. callback
  239. )
  240. )
  241. }
  242. })(file)
  243. }
  244. return async.series(jobs, function(error) {
  245. if (error != null) {
  246. return callback(error)
  247. }
  248. return callback(null, outputFiles, allFiles)
  249. })
  250. })
  251. },
  252. _deleteFileIfNotDirectory(path, callback) {
  253. if (callback == null) {
  254. callback = function(error) {}
  255. }
  256. return fs.stat(path, function(error, stat) {
  257. if (error != null && error.code === 'ENOENT') {
  258. return callback()
  259. } else if (error != null) {
  260. logger.err(
  261. { err: error, path },
  262. 'error stating file in deleteFileIfNotDirectory'
  263. )
  264. return callback(error)
  265. } else if (stat.isFile()) {
  266. return fs.unlink(path, function(error) {
  267. if (error != null) {
  268. logger.err(
  269. { err: error, path },
  270. 'error removing file in deleteFileIfNotDirectory'
  271. )
  272. return callback(error)
  273. } else {
  274. return callback()
  275. }
  276. })
  277. } else {
  278. return callback()
  279. }
  280. })
  281. },
  282. _writeResourceToDisk(project_id, resource, basePath, callback) {
  283. if (callback == null) {
  284. callback = function(error) {}
  285. }
  286. return ResourceWriter.checkPath(basePath, resource.path, function(
  287. error,
  288. path
  289. ) {
  290. if (error != null) {
  291. return callback(error)
  292. }
  293. return fs.mkdir(Path.dirname(path), { recursive: true }, function(error) {
  294. if (error != null) {
  295. return callback(error)
  296. }
  297. // TODO: Don't overwrite file if it hasn't been modified
  298. if (resource.url != null) {
  299. return UrlCache.downloadUrlToFile(
  300. project_id,
  301. resource.url,
  302. path,
  303. resource.modified,
  304. function(err) {
  305. if (err != null) {
  306. logger.err(
  307. {
  308. err,
  309. project_id,
  310. path,
  311. resource_url: resource.url,
  312. modified: resource.modified
  313. },
  314. 'error downloading file for resources'
  315. )
  316. }
  317. return callback()
  318. }
  319. ) // try and continue compiling even if http resource can not be downloaded at this time
  320. } else {
  321. const process = require('process')
  322. fs.writeFile(path, resource.content, callback)
  323. try {
  324. let result
  325. return (result = fs.lstatSync(path))
  326. } catch (e) {}
  327. }
  328. })
  329. })
  330. },
  331. checkPath(basePath, resourcePath, callback) {
  332. const path = Path.normalize(Path.join(basePath, resourcePath))
  333. if (path.slice(0, basePath.length + 1) !== basePath + '/') {
  334. return callback(new Error('resource path is outside root directory'))
  335. } else {
  336. return callback(null, path)
  337. }
  338. }
  339. }