UrlCache.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-return-assign,
  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. let UrlCache
  16. const db = require('./db')
  17. const dbQueue = require('./DbQueue')
  18. const UrlFetcher = require('./UrlFetcher')
  19. const Settings = require('@overleaf/settings')
  20. const crypto = require('crypto')
  21. const fs = require('fs')
  22. const logger = require('logger-sharelatex')
  23. const async = require('async')
  24. const Metrics = require('./Metrics')
  25. module.exports = UrlCache = {
  26. downloadUrlToFile(project_id, url, destPath, lastModified, callback) {
  27. if (callback == null) {
  28. callback = function (error) {}
  29. }
  30. return UrlCache._ensureUrlIsInCache(
  31. project_id,
  32. url,
  33. lastModified,
  34. (error, pathToCachedUrl) => {
  35. if (error != null) {
  36. return callback(error)
  37. }
  38. return fs.copyFile(pathToCachedUrl, destPath, function (error) {
  39. if (error != null) {
  40. logger.error(
  41. { err: error, from: pathToCachedUrl, to: destPath },
  42. 'error copying file from cache'
  43. )
  44. return UrlCache._clearUrlDetails(project_id, url, () =>
  45. callback(error)
  46. )
  47. } else {
  48. return callback(error)
  49. }
  50. })
  51. }
  52. )
  53. },
  54. clearProject(project_id, callback) {
  55. if (callback == null) {
  56. callback = function (error) {}
  57. }
  58. return UrlCache._findAllUrlsInProject(project_id, function (error, urls) {
  59. logger.log(
  60. { project_id, url_count: urls.length },
  61. 'clearing project URLs'
  62. )
  63. if (error != null) {
  64. return callback(error)
  65. }
  66. const jobs = Array.from(urls || []).map(url =>
  67. (
  68. url => callback =>
  69. UrlCache._clearUrlFromCache(project_id, url, function (error) {
  70. if (error != null) {
  71. logger.error(
  72. { err: error, project_id, url },
  73. 'error clearing project URL'
  74. )
  75. }
  76. return callback()
  77. })
  78. )(url)
  79. )
  80. return async.series(jobs, callback)
  81. })
  82. },
  83. _ensureUrlIsInCache(project_id, url, lastModified, callback) {
  84. if (callback == null) {
  85. callback = function (error, pathOnDisk) {}
  86. }
  87. if (lastModified != null) {
  88. // MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds.
  89. // So round down to seconds
  90. lastModified = new Date(Math.floor(lastModified.getTime() / 1000) * 1000)
  91. }
  92. return UrlCache._doesUrlNeedDownloading(
  93. project_id,
  94. url,
  95. lastModified,
  96. (error, needsDownloading) => {
  97. if (error != null) {
  98. return callback(error)
  99. }
  100. if (needsDownloading) {
  101. logger.log({ url, lastModified }, 'downloading URL')
  102. return UrlFetcher.pipeUrlToFileWithRetry(
  103. url,
  104. UrlCache._cacheFilePathForUrl(project_id, url),
  105. error => {
  106. if (error != null) {
  107. return callback(error)
  108. }
  109. return UrlCache._updateOrCreateUrlDetails(
  110. project_id,
  111. url,
  112. lastModified,
  113. error => {
  114. if (error != null) {
  115. return callback(error)
  116. }
  117. return callback(
  118. null,
  119. UrlCache._cacheFilePathForUrl(project_id, url)
  120. )
  121. }
  122. )
  123. }
  124. )
  125. } else {
  126. logger.log({ url, lastModified }, 'URL is up to date in cache')
  127. return callback(null, UrlCache._cacheFilePathForUrl(project_id, url))
  128. }
  129. }
  130. )
  131. },
  132. _doesUrlNeedDownloading(project_id, url, lastModified, callback) {
  133. if (callback == null) {
  134. callback = function (error, needsDownloading) {}
  135. }
  136. if (lastModified == null) {
  137. return callback(null, true)
  138. }
  139. return UrlCache._findUrlDetails(
  140. project_id,
  141. url,
  142. function (error, urlDetails) {
  143. if (error != null) {
  144. return callback(error)
  145. }
  146. if (
  147. urlDetails == null ||
  148. urlDetails.lastModified == null ||
  149. urlDetails.lastModified.getTime() < lastModified.getTime()
  150. ) {
  151. return callback(null, true)
  152. } else {
  153. return callback(null, false)
  154. }
  155. }
  156. )
  157. },
  158. _cacheFileNameForUrl(project_id, url) {
  159. return project_id + ':' + crypto.createHash('md5').update(url).digest('hex')
  160. },
  161. _cacheFilePathForUrl(project_id, url) {
  162. return `${Settings.path.clsiCacheDir}/${UrlCache._cacheFileNameForUrl(
  163. project_id,
  164. url
  165. )}`
  166. },
  167. _clearUrlFromCache(project_id, url, callback) {
  168. if (callback == null) {
  169. callback = function (error) {}
  170. }
  171. return UrlCache._clearUrlDetails(project_id, url, function (error) {
  172. if (error != null) {
  173. return callback(error)
  174. }
  175. return UrlCache._deleteUrlCacheFromDisk(
  176. project_id,
  177. url,
  178. function (error) {
  179. if (error != null) {
  180. return callback(error)
  181. }
  182. return callback(null)
  183. }
  184. )
  185. })
  186. },
  187. _deleteUrlCacheFromDisk(project_id, url, callback) {
  188. if (callback == null) {
  189. callback = function (error) {}
  190. }
  191. return fs.unlink(
  192. UrlCache._cacheFilePathForUrl(project_id, url),
  193. function (error) {
  194. if (error != null && error.code !== 'ENOENT') {
  195. // no error if the file isn't present
  196. return callback(error)
  197. } else {
  198. return callback()
  199. }
  200. }
  201. )
  202. },
  203. _findUrlDetails(project_id, url, callback) {
  204. if (callback == null) {
  205. callback = function (error, urlDetails) {}
  206. }
  207. const timer = new Metrics.Timer('db-find-url-details')
  208. const job = cb =>
  209. db.UrlCache.findOne({ where: { url, project_id } })
  210. .then(urlDetails => cb(null, urlDetails))
  211. .error(cb)
  212. dbQueue.queue.push(job, (error, urlDetails) => {
  213. timer.done()
  214. callback(error, urlDetails)
  215. })
  216. },
  217. _updateOrCreateUrlDetails(project_id, url, lastModified, callback) {
  218. if (callback == null) {
  219. callback = function (error) {}
  220. }
  221. const timer = new Metrics.Timer('db-update-or-create-url-details')
  222. const job = cb =>
  223. db.UrlCache.findOrCreate({ where: { url, project_id } })
  224. .spread((urlDetails, created) =>
  225. urlDetails
  226. .update({ lastModified })
  227. .then(() => cb())
  228. .error(cb)
  229. )
  230. .error(cb)
  231. dbQueue.queue.push(job, error => {
  232. timer.done()
  233. callback(error)
  234. })
  235. },
  236. _clearUrlDetails(project_id, url, callback) {
  237. if (callback == null) {
  238. callback = function (error) {}
  239. }
  240. const timer = new Metrics.Timer('db-clear-url-details')
  241. const job = cb =>
  242. db.UrlCache.destroy({ where: { url, project_id } })
  243. .then(() => cb(null))
  244. .error(cb)
  245. dbQueue.queue.push(job, error => {
  246. timer.done()
  247. callback(error)
  248. })
  249. },
  250. _findAllUrlsInProject(project_id, callback) {
  251. if (callback == null) {
  252. callback = function (error, urls) {}
  253. }
  254. const timer = new Metrics.Timer('db-find-urls-in-project')
  255. const job = cb =>
  256. db.UrlCache.findAll({ where: { project_id } })
  257. .then(urlEntries =>
  258. cb(
  259. null,
  260. urlEntries.map(entry => entry.url)
  261. )
  262. )
  263. .error(cb)
  264. dbQueue.queue.push(job, (err, urls) => {
  265. timer.done()
  266. callback(err, urls)
  267. })
  268. },
  269. }