OutputCacheManager.coffee 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. async = require "async"
  2. fs = require "fs"
  3. fse = require "fs-extra"
  4. Path = require "path"
  5. logger = require "logger-sharelatex"
  6. _ = require "underscore"
  7. OutputFileOptimiser = require "./OutputFileOptimiser"
  8. module.exports = OutputCacheManager =
  9. CACHE_SUBDIR: '.cache/clsi'
  10. BUILD_REGEX: /^[0-9a-f]+$/ # build id is Date.now() converted to hex
  11. CACHE_LIMIT: 2 # maximum number of cache directories
  12. CACHE_AGE: 60*60*1000 # up to one hour old
  13. path: (buildId, file) ->
  14. # used by static server, given build id return '.cache/clsi/buildId'
  15. if buildId.match OutputCacheManager.BUILD_REGEX
  16. return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId, file)
  17. else
  18. # for invalid build id, return top level
  19. return file
  20. saveOutputFiles: (outputFiles, compileDir, callback = (error) ->) ->
  21. # make a compileDir/CACHE_SUBDIR/build_id directory and
  22. # copy all the output files into it
  23. cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR)
  24. # Put the files into a new cache subdirectory
  25. buildId = Date.now().toString(16)
  26. cacheDir = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR, buildId)
  27. # let file expiry run in the background
  28. OutputCacheManager.expireOutputFiles cacheRoot, {keep: buildId}
  29. checkFile = (src, callback) ->
  30. # check if we have a valid file to copy into the cache
  31. fs.stat src, (err, stats) ->
  32. if err?
  33. # some problem reading the file
  34. logger.error err: err, file: src, "stat error for file in cache"
  35. callback(err)
  36. else if not stats.isFile()
  37. # other filetype - reject it
  38. logger.error err: err, src: src, dst: dst, stat: stats, "nonfile output - refusing to copy to cache"
  39. callback(new Error("output file is not a file"), file)
  40. else
  41. # it's a plain file, ok to copy
  42. callback(null)
  43. copyFile = (src, dst, callback) ->
  44. # copy output file into the cache
  45. fse.copy src, dst, (err) ->
  46. if err?
  47. logger.error err: err, src: src, dst: dst, "copy error for file in cache"
  48. callback(err)
  49. else
  50. # call the optimiser for the file too
  51. OutputFileOptimiser.optimiseFile src, dst, callback
  52. # make the new cache directory
  53. fse.ensureDir cacheDir, (err) ->
  54. if err?
  55. logger.error err: err, directory: cacheDir, "error creating cache directory"
  56. callback(err, outputFiles)
  57. else
  58. # copy all the output files into the new cache directory
  59. async.mapSeries outputFiles, (file, cb) ->
  60. newFile = _.clone(file)
  61. [src, dst] = [Path.join(compileDir, file.path), Path.join(cacheDir, file.path)]
  62. checkFile src, (err) ->
  63. copyFile src, dst, (err) ->
  64. if not err?
  65. newFile.build = buildId # attach a build id if we cached the file
  66. cb(err, newFile)
  67. , (err, results) ->
  68. if err?
  69. # pass back the original files if we encountered *any* error
  70. callback(err, outputFiles)
  71. else
  72. # pass back the list of new files in the cache
  73. callback(err, results)
  74. expireOutputFiles: (cacheRoot, options, callback = (error) ->) ->
  75. # look in compileDir for build dirs and delete if > N or age of mod time > T
  76. fs.readdir cacheRoot, (err, results) ->
  77. if err?
  78. return callback(null) if err.code == 'ENOENT' # cache directory is empty
  79. logger.error err: err, project_id: cacheRoot, "error clearing cache"
  80. return callback(err)
  81. dirs = results.sort().reverse()
  82. currentTime = Date.now()
  83. isExpired = (dir, index) ->
  84. return false if options?.keep == dir
  85. # remove any directories over the hard limit
  86. return true if index > OutputCacheManager.CACHE_LIMIT
  87. # we can get the build time from the directory name
  88. dirTime = parseInt(dir, 16)
  89. age = currentTime - dirTime
  90. return age > OutputCacheManager.CACHE_AGE
  91. toRemove = _.filter(dirs, isExpired)
  92. removeDir = (dir, cb) ->
  93. fse.remove Path.join(cacheRoot, dir), (err, result) ->
  94. logger.log cache: cacheRoot, dir: dir, "removed expired cache dir"
  95. if err?
  96. logger.error err: err, dir: dir, "cache remove error"
  97. cb(err, result)
  98. async.eachSeries toRemove, (dir, cb) ->
  99. removeDir dir, cb
  100. , callback