ArchiveManager.coffee 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. logger = require "logger-sharelatex"
  2. metrics = require "metrics-sharelatex"
  3. fs = require "fs"
  4. Path = require "path"
  5. fse = require "fs-extra"
  6. yauzl = require "yauzl"
  7. Settings = require "settings-sharelatex"
  8. _ = require("underscore")
  9. ONE_MEG = 1024 * 1024
  10. module.exports = ArchiveManager =
  11. _isZipTooLarge: (source, callback = (err, isTooLarge)->)->
  12. callback = _.once callback
  13. totalSizeInBytes = null
  14. yauzl.open source, {lazyEntries: true}, (err, zipfile) ->
  15. return callback(err) if err?
  16. if Settings.maxEntitiesPerProject? and zipfile.entryCount > Settings.maxEntitiesPerProject
  17. return callback(null, true) # too many files in zip file
  18. zipfile.on "error", callback
  19. # read all the entries
  20. zipfile.readEntry()
  21. zipfile.on "entry", (entry) ->
  22. totalSizeInBytes += entry.uncompressedSize
  23. zipfile.readEntry() # get the next entry
  24. # no more entries to read
  25. zipfile.on "end", () ->
  26. if !totalSizeInBytes? or isNaN(totalSizeInBytes)
  27. logger.err source:source, totalSizeInBytes:totalSizeInBytes, "error getting bytes of zip"
  28. return callback(new Error("error getting bytes of zip"))
  29. isTooLarge = totalSizeInBytes > (ONE_MEG * 300)
  30. callback(null, isTooLarge)
  31. _checkFilePath: (entry, destination, callback = (err, destFile) ->) ->
  32. # check if the entry is a directory
  33. endsWithSlash = /\/$/
  34. if endsWithSlash.test(entry.fileName)
  35. return callback() # don't give a destfile for directory
  36. # check that the file does not use a relative path
  37. for dir in entry.fileName.split('/')
  38. if dir == '..'
  39. return callback(new Error("relative path"))
  40. # check that the destination file path is normalized
  41. dest = "#{destination}/#{entry.fileName}"
  42. if dest != Path.normalize(dest)
  43. return callback(new Error("unnormalized path"))
  44. else
  45. return callback(null, dest)
  46. _writeFileEntry: (zipfile, entry, destFile, callback = (err)->) ->
  47. callback = _.once callback
  48. zipfile.openReadStream entry, (err, readStream) ->
  49. return callback(err) if err?
  50. readStream.on "error", callback
  51. readStream.on "end", callback
  52. errorHandler = (err) -> # clean up before calling callback
  53. readStream.unpipe()
  54. readStream.destroy()
  55. callback(err)
  56. fse.ensureDir Path.dirname(destFile), (err) ->
  57. return errorHandler(err) if err?
  58. writeStream = fs.createWriteStream destFile
  59. writeStream.on 'error', errorHandler
  60. readStream.pipe(writeStream)
  61. _extractZipFiles: (source, destination, callback = (err) ->) ->
  62. callback = _.once callback
  63. yauzl.open source, {lazyEntries: true}, (err, zipfile) ->
  64. return callback(err) if err?
  65. zipfile.on "error", callback
  66. # read all the entries
  67. zipfile.readEntry()
  68. zipfile.on "entry", (entry) ->
  69. logger.log {source:source, fileName: entry.fileName}, "processing zip file entry"
  70. ArchiveManager._checkFilePath entry, destination, (err, destFile) ->
  71. if err?
  72. logger.warn err:err, source:source, destination:destination, "skipping bad file path"
  73. zipfile.readEntry() # bad path, just skip to the next file
  74. return
  75. if destFile? # only write files
  76. ArchiveManager._writeFileEntry zipfile, entry, destFile, (err) ->
  77. if err?
  78. logger.error err:err, source:source, destFile:destFile, "error unzipping file entry"
  79. zipfile.close() # bail out, stop reading file entries
  80. return callback(err)
  81. else
  82. zipfile.readEntry() # continue to the next file
  83. else # if it's a directory, continue
  84. zipfile.readEntry()
  85. # no more entries to read
  86. zipfile.on "end", callback
  87. extractZipArchive: (source, destination, _callback = (err) ->) ->
  88. callback = (args...) ->
  89. _callback(args...)
  90. _callback = () ->
  91. ArchiveManager._isZipTooLarge source, (err, isTooLarge)->
  92. if err?
  93. logger.err err:err, "error checking size of zip file"
  94. return callback(err)
  95. if isTooLarge
  96. return callback(new Error("zip_too_large"))
  97. timer = new metrics.Timer("unzipDirectory")
  98. logger.log source: source, destination: destination, "unzipping file"
  99. ArchiveManager._extractZipFiles source, destination, (err) ->
  100. timer.done()
  101. if err?
  102. logger.error {err, source, destination}, "unzip failed"
  103. callback(err)
  104. else
  105. callback()
  106. findTopLevelDirectory: (directory, callback = (error, topLevelDir) ->) ->
  107. fs.readdir directory, (error, files) ->
  108. return callback(error) if error?
  109. if files.length == 1
  110. childPath = Path.join(directory, files[0])
  111. fs.stat childPath, (error, stat) ->
  112. return callback(error) if error?
  113. if stat.isDirectory()
  114. return callback(null, childPath)
  115. else
  116. return callback(null, directory)
  117. else
  118. return callback(null, directory)