FileStoreHandler.coffee 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. logger = require("logger-sharelatex")
  2. fs = require("fs")
  3. request = require("request")
  4. settings = require("settings-sharelatex")
  5. Async = require('async')
  6. FileHashManager = require("./FileHashManager")
  7. File = require('../../models/File').File
  8. oneMinInMs = 60 * 1000
  9. fiveMinsInMs = oneMinInMs * 5
  10. module.exports = FileStoreHandler =
  11. RETRY_ATTEMPTS: 3
  12. uploadFileFromDisk: (project_id, file_args, fsPath, callback = (error, url, fileRef) ->)->
  13. fs.lstat fsPath, (err, stat)->
  14. if err?
  15. logger.err err:err, project_id:project_id, file_args:file_args, fsPath:fsPath, "error stating file"
  16. callback(err)
  17. if !stat?
  18. logger.err project_id:project_id, file_args:file_args, fsPath:fsPath, "stat is not available, can not check file from disk"
  19. return callback(new Error("error getting stat, not available"))
  20. if !stat.isFile()
  21. logger.log project_id:project_id, file_args:file_args, fsPath:fsPath, "tried to upload symlink, not contining"
  22. return callback(new Error("can not upload symlink"))
  23. Async.retry FileStoreHandler.RETRY_ATTEMPTS, (cb, results) ->
  24. FileStoreHandler._doUploadFileFromDisk project_id, file_args, fsPath, cb
  25. , (err, result) ->
  26. if err?
  27. logger.err {err, project_id, file_args}, "Error uploading file, retries failed"
  28. return callback(err)
  29. callback(err, result.url, result.fileRef)
  30. _doUploadFileFromDisk: (project_id, file_args, fsPath, callback = (err, result)->) ->
  31. _cb = callback
  32. callback = (err, result...) ->
  33. callback = -> # avoid double callbacks
  34. _cb(err, result...)
  35. FileHashManager.computeHash fsPath, (err, hashValue) ->
  36. return callback(err) if err?
  37. fileRef = new File(Object.assign({}, file_args, {hash: hashValue}))
  38. file_id = fileRef._id
  39. logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, hash: hashValue, fileRef:fileRef, "uploading file from disk"
  40. readStream = fs.createReadStream(fsPath)
  41. readStream.on "error", (err)->
  42. logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
  43. callback err
  44. readStream.on "open", () ->
  45. url = FileStoreHandler._buildUrl(project_id, file_id)
  46. opts =
  47. method: "post"
  48. uri: url
  49. timeout:fiveMinsInMs
  50. headers:
  51. "X-File-Hash-From-Web": hashValue # send the hash to the filestore as a custom header so it can be checked
  52. writeStream = request(opts)
  53. writeStream.on "error", (err)->
  54. logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
  55. callback err
  56. writeStream.on 'response', (response) ->
  57. if response.statusCode not in [200, 201]
  58. err = new Error("non-ok response from filestore for upload: #{response.statusCode}")
  59. logger.err {err, statusCode: response.statusCode}, "error uploading to filestore"
  60. callback(err)
  61. else
  62. callback(null, {url, fileRef}) # have to pass back an object because async.retry only accepts a single result argument
  63. readStream.pipe writeStream
  64. getFileStream: (project_id, file_id, query, callback)->
  65. logger.log project_id:project_id, file_id:file_id, query:query, "getting file stream from file store"
  66. queryString = ""
  67. if query? and query["format"]?
  68. queryString = "?format=#{query['format']}"
  69. opts =
  70. method : "get"
  71. uri: "#{@_buildUrl(project_id, file_id)}#{queryString}"
  72. timeout:fiveMinsInMs
  73. headers: {}
  74. if query? and query['range']?
  75. rangeText = query['range']
  76. if rangeText && rangeText.match? && rangeText.match(/\d+-\d+/)
  77. opts.headers['range'] = "bytes=#{query['range']}"
  78. readStream = request(opts)
  79. readStream.on "error", (err) ->
  80. logger.err {err, project_id, file_id, query, opts}, "error in file stream"
  81. callback(null, readStream)
  82. deleteFile: (project_id, file_id, callback)->
  83. logger.log project_id:project_id, file_id:file_id, "telling file store to delete file"
  84. opts =
  85. method : "delete"
  86. uri: @_buildUrl(project_id, file_id)
  87. timeout:fiveMinsInMs
  88. request opts, (err, response)->
  89. if err?
  90. logger.err err:err, project_id:project_id, file_id:file_id, "something went wrong deleting file from filestore"
  91. callback(err)
  92. copyFile: (oldProject_id, oldFile_id, newProject_id, newFile_id, callback)->
  93. logger.log oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "telling filestore to copy a file"
  94. opts =
  95. method : "put"
  96. json:
  97. source:
  98. project_id:oldProject_id
  99. file_id:oldFile_id
  100. uri: @_buildUrl(newProject_id, newFile_id)
  101. timeout:fiveMinsInMs
  102. request opts, (err, response)->
  103. if err?
  104. logger.err err:err, oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "something went wrong telling filestore api to copy file"
  105. callback(err)
  106. else if 200 <= response.statusCode < 300
  107. # successful response
  108. callback(null, opts.uri)
  109. else
  110. err = new Error("non-ok response from filestore for copyFile: #{response.statusCode}")
  111. logger.err {uri: opts.uri, statusCode: response.statusCode}, "error uploading to filestore"
  112. callback(err)
  113. _buildUrl: (project_id, file_id)->
  114. return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}"