UrlCache.coffee 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. db = require("./db")
  2. dbQueue = require "./DbQueue"
  3. UrlFetcher = require("./UrlFetcher")
  4. Settings = require("settings-sharelatex")
  5. crypto = require("crypto")
  6. fs = require("fs")
  7. logger = require "logger-sharelatex"
  8. async = require "async"
  9. module.exports = UrlCache =
  10. downloadUrlToFile: (project_id, url, destPath, lastModified, callback = (error) ->) ->
  11. UrlCache._ensureUrlIsInCache project_id, url, lastModified, (error, pathToCachedUrl) =>
  12. return callback(error) if error?
  13. UrlCache._copyFile pathToCachedUrl, destPath, (error) ->
  14. if error?
  15. UrlCache._clearUrlDetails project_id, url, () ->
  16. callback(error)
  17. else
  18. callback(error)
  19. clearProject: (project_id, callback = (error) ->) ->
  20. UrlCache._findAllUrlsInProject project_id, (error, urls) ->
  21. logger.log project_id: project_id, url_count: urls.length, "clearing project URLs"
  22. return callback(error) if error?
  23. jobs = for url in (urls or [])
  24. do (url) ->
  25. (callback) ->
  26. UrlCache._clearUrlFromCache project_id, url, (error) ->
  27. if error?
  28. logger.error err: error, project_id: project_id, url: url, "error clearing project URL"
  29. callback()
  30. async.series jobs, callback
  31. _ensureUrlIsInCache: (project_id, url, lastModified, callback = (error, pathOnDisk) ->) ->
  32. if lastModified?
  33. # MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds.
  34. # So round down to seconds
  35. lastModified = new Date(Math.floor(lastModified.getTime() / 1000) * 1000)
  36. UrlCache._doesUrlNeedDownloading project_id, url, lastModified, (error, needsDownloading) =>
  37. return callback(error) if error?
  38. if needsDownloading
  39. logger.log url: url, lastModified: lastModified, "downloading URL"
  40. UrlFetcher.pipeUrlToFile url, UrlCache._cacheFilePathForUrl(project_id, url), (error) =>
  41. return callback(error) if error?
  42. UrlCache._updateOrCreateUrlDetails project_id, url, lastModified, (error) =>
  43. return callback(error) if error?
  44. callback null, UrlCache._cacheFilePathForUrl(project_id, url)
  45. else
  46. logger.log url: url, lastModified: lastModified, "URL is up to date in cache"
  47. callback null, UrlCache._cacheFilePathForUrl(project_id, url)
  48. _doesUrlNeedDownloading: (project_id, url, lastModified, callback = (error, needsDownloading) ->) ->
  49. if !lastModified?
  50. return callback null, true
  51. UrlCache._findUrlDetails project_id, url, (error, urlDetails) ->
  52. return callback(error) if error?
  53. if !urlDetails? or !urlDetails.lastModified? or urlDetails.lastModified.getTime() < lastModified.getTime()
  54. return callback null, true
  55. else
  56. return callback null, false
  57. _cacheFileNameForUrl: (project_id, url) ->
  58. project_id + ":" + crypto.createHash("md5").update(url).digest("hex")
  59. _cacheFilePathForUrl: (project_id, url) ->
  60. "#{Settings.path.clsiCacheDir}/#{UrlCache._cacheFileNameForUrl(project_id, url)}"
  61. _copyFile: (from, to, _callback = (error) ->) ->
  62. callbackOnce = (error) ->
  63. if error?
  64. logger.error err: error, from:from, to:to, "error copying file from cache"
  65. _callback(error)
  66. _callback = () ->
  67. writeStream = fs.createWriteStream(to)
  68. readStream = fs.createReadStream(from)
  69. writeStream.on "error", callbackOnce
  70. readStream.on "error", callbackOnce
  71. writeStream.on "close", callbackOnce
  72. writeStream.on "open", () ->
  73. readStream.pipe(writeStream)
  74. _clearUrlFromCache: (project_id, url, callback = (error) ->) ->
  75. UrlCache._clearUrlDetails project_id, url, (error) ->
  76. return callback(error) if error?
  77. UrlCache._deleteUrlCacheFromDisk project_id, url, (error) ->
  78. return callback(error) if error?
  79. callback null
  80. _deleteUrlCacheFromDisk: (project_id, url, callback = (error) ->) ->
  81. fs.unlink UrlCache._cacheFilePathForUrl(project_id, url), (error) ->
  82. if error? and error.code != 'ENOENT' # no error if the file isn't present
  83. return callback(error)
  84. else
  85. return callback()
  86. _findUrlDetails: (project_id, url, callback = (error, urlDetails) ->) ->
  87. job = (cb)->
  88. db.UrlCache.find(where: { url: url, project_id: project_id })
  89. .then((urlDetails) -> cb null, urlDetails)
  90. .error cb
  91. dbQueue.queue.push job, callback
  92. _updateOrCreateUrlDetails: (project_id, url, lastModified, callback = (error) ->) ->
  93. job = (cb)->
  94. db.UrlCache.findOrCreate(where: {url: url, project_id: project_id})
  95. .spread(
  96. (urlDetails, created) ->
  97. urlDetails.updateAttributes(lastModified: lastModified)
  98. .then(() -> cb())
  99. .error(cb)
  100. )
  101. .error cb
  102. dbQueue.queue.push(job, callback)
  103. _clearUrlDetails: (project_id, url, callback = (error) ->) ->
  104. job = (cb)->
  105. db.UrlCache.destroy(where: {url: url, project_id: project_id})
  106. .then(() -> cb null)
  107. .error cb
  108. dbQueue.queue.push(job, callback)
  109. _findAllUrlsInProject: (project_id, callback = (error, urls) ->) ->
  110. job = (cb)->
  111. db.UrlCache.findAll(where: { project_id: project_id })
  112. .then(
  113. (urlEntries) ->
  114. cb null, urlEntries.map((entry) -> entry.url)
  115. )
  116. .error cb
  117. dbQueue.queue.push(job, callback)