FileStoreHandler.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. const _ = require('underscore')
  2. const logger = require('logger-sharelatex')
  3. const fs = require('fs')
  4. const request = require('request')
  5. const settings = require('settings-sharelatex')
  6. const Async = require('async')
  7. const FileHashManager = require('./FileHashManager')
  8. const { File } = require('../../models/File')
  9. const Errors = require('../Errors/Errors')
  10. const ONE_MIN_IN_MS = 60 * 1000
  11. const FIVE_MINS_IN_MS = ONE_MIN_IN_MS * 5
  12. const FileStoreHandler = {
  13. RETRY_ATTEMPTS: 3,
  14. uploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
  15. fs.lstat(fsPath, function(err, stat) {
  16. if (err) {
  17. logger.warn({ err, projectId, fileArgs, fsPath }, 'error stating file')
  18. callback(err)
  19. }
  20. if (!stat) {
  21. logger.warn(
  22. { projectId, fileArgs, fsPath },
  23. 'stat is not available, can not check file from disk'
  24. )
  25. return callback(new Error('error getting stat, not available'))
  26. }
  27. if (!stat.isFile()) {
  28. logger.log(
  29. { projectId, fileArgs, fsPath },
  30. 'tried to upload symlink, not continuing'
  31. )
  32. return callback(new Error('can not upload symlink'))
  33. }
  34. Async.retry(
  35. FileStoreHandler.RETRY_ATTEMPTS,
  36. (cb, results) =>
  37. FileStoreHandler._doUploadFileFromDisk(
  38. projectId,
  39. fileArgs,
  40. fsPath,
  41. cb
  42. ),
  43. function(err, result) {
  44. if (err) {
  45. logger.warn(
  46. { err, projectId, fileArgs },
  47. 'Error uploading file, retries failed'
  48. )
  49. return callback(err)
  50. }
  51. callback(err, result.url, result.fileRef)
  52. }
  53. )
  54. })
  55. },
  56. _doUploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
  57. const callbackOnce = _.once(callback)
  58. FileHashManager.computeHash(fsPath, function(err, hashValue) {
  59. if (err) {
  60. return callbackOnce(err)
  61. }
  62. const fileRef = new File(Object.assign({}, fileArgs, { hash: hashValue }))
  63. const fileId = fileRef._id
  64. const readStream = fs.createReadStream(fsPath)
  65. readStream.on('error', function(err) {
  66. logger.warn(
  67. { err, projectId, fileId, fsPath },
  68. 'something went wrong on the read stream of uploadFileFromDisk'
  69. )
  70. callbackOnce(err)
  71. })
  72. readStream.on('open', function() {
  73. const url = FileStoreHandler._buildUrl(projectId, fileId)
  74. const opts = {
  75. method: 'post',
  76. uri: url,
  77. timeout: FIVE_MINS_IN_MS,
  78. headers: {
  79. 'X-File-Hash-From-Web': hashValue
  80. } // send the hash to the filestore as a custom header so it can be checked
  81. }
  82. const writeStream = request(opts)
  83. writeStream.on('error', function(err) {
  84. logger.warn(
  85. { err, projectId, fileId, fsPath },
  86. 'something went wrong on the write stream of uploadFileFromDisk'
  87. )
  88. callbackOnce(err)
  89. })
  90. writeStream.on('response', function(response) {
  91. if (![200, 201].includes(response.statusCode)) {
  92. err = new Error(
  93. `non-ok response from filestore for upload: ${
  94. response.statusCode
  95. }`
  96. )
  97. logger.warn(
  98. { err, statusCode: response.statusCode },
  99. 'error uploading to filestore'
  100. )
  101. return callbackOnce(err)
  102. }
  103. callbackOnce(null, { url, fileRef })
  104. }) // have to pass back an object because async.retry only accepts a single result argument
  105. readStream.pipe(writeStream)
  106. })
  107. })
  108. },
  109. getFileStream(projectId, fileId, query, callback) {
  110. let queryString = ''
  111. if (query != null && query['format'] != null) {
  112. queryString = `?format=${query['format']}`
  113. }
  114. const opts = {
  115. method: 'get',
  116. uri: `${this._buildUrl(projectId, fileId)}${queryString}`,
  117. timeout: FIVE_MINS_IN_MS,
  118. headers: {}
  119. }
  120. if (query != null && query['range'] != null) {
  121. const rangeText = query['range']
  122. if (rangeText && rangeText.match != null && rangeText.match(/\d+-\d+/)) {
  123. opts.headers['range'] = `bytes=${query['range']}`
  124. }
  125. }
  126. const readStream = request(opts)
  127. readStream.on('error', err =>
  128. logger.err(
  129. { err, projectId, fileId, query, opts },
  130. 'error in file stream'
  131. )
  132. )
  133. return callback(null, readStream)
  134. },
  135. getFileSize(projectId, fileId, callback) {
  136. const url = this._buildUrl(projectId, fileId)
  137. request.head(url, (err, res) => {
  138. if (err) {
  139. logger.warn(
  140. { err, projectId, fileId },
  141. 'failed to get file size from filestore'
  142. )
  143. return callback(err)
  144. }
  145. if (res.statusCode === 404) {
  146. return callback(new Errors.NotFoundError('file not found in filestore'))
  147. }
  148. if (res.statusCode !== 200) {
  149. logger.warn(
  150. { projectId, fileId, statusCode: res.statusCode },
  151. 'filestore returned non-200 response'
  152. )
  153. return callback(new Error('filestore returned non-200 response'))
  154. }
  155. const fileSize = res.headers['content-length']
  156. callback(null, fileSize)
  157. })
  158. },
  159. deleteFile(projectId, fileId, callback) {
  160. logger.log({ projectId, fileId }, 'telling file store to delete file')
  161. const opts = {
  162. method: 'delete',
  163. uri: this._buildUrl(projectId, fileId),
  164. timeout: FIVE_MINS_IN_MS
  165. }
  166. return request(opts, function(err, response) {
  167. if (err) {
  168. logger.warn(
  169. { err, projectId, fileId },
  170. 'something went wrong deleting file from filestore'
  171. )
  172. }
  173. return callback(err)
  174. })
  175. },
  176. copyFile(oldProjectId, oldFileId, newProjectId, newFileId, callback) {
  177. logger.log(
  178. { oldProjectId, oldFileId, newProjectId, newFileId },
  179. 'telling filestore to copy a file'
  180. )
  181. const opts = {
  182. method: 'put',
  183. json: {
  184. source: {
  185. project_id: oldProjectId,
  186. file_id: oldFileId
  187. }
  188. },
  189. uri: this._buildUrl(newProjectId, newFileId),
  190. timeout: FIVE_MINS_IN_MS
  191. }
  192. return request(opts, function(err, response) {
  193. if (err) {
  194. logger.warn(
  195. { err, oldProjectId, oldFileId, newProjectId, newFileId },
  196. 'something went wrong telling filestore api to copy file'
  197. )
  198. return callback(err)
  199. } else if (response.statusCode >= 200 && response.statusCode < 300) {
  200. // successful response
  201. return callback(null, opts.uri)
  202. } else {
  203. err = new Error(
  204. `non-ok response from filestore for copyFile: ${response.statusCode}`
  205. )
  206. logger.warn(
  207. { uri: opts.uri, statusCode: response.statusCode },
  208. 'error uploading to filestore'
  209. )
  210. return callback(err)
  211. }
  212. })
  213. },
  214. _buildUrl(projectId, fileId) {
  215. return `${settings.apis.filestore.url}/project/${projectId}/file/${fileId}`
  216. }
  217. }
  218. module.exports = FileStoreHandler