FileStoreHandler.js 7.5 KB

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