|
@@ -5,6 +5,8 @@ const request = require('request')
|
|
|
const settings = require('@overleaf/settings')
|
|
const settings = require('@overleaf/settings')
|
|
|
const Async = require('async')
|
|
const Async = require('async')
|
|
|
const FileHashManager = require('./FileHashManager')
|
|
const FileHashManager = require('./FileHashManager')
|
|
|
|
|
+const HistoryManager = require('../History/HistoryManager')
|
|
|
|
|
+const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
|
|
const { File } = require('../../models/File')
|
|
const { File } = require('../../models/File')
|
|
|
const Errors = require('../Errors/Errors')
|
|
const Errors = require('../Errors/Errors')
|
|
|
const OError = require('@overleaf/o-error')
|
|
const OError = require('@overleaf/o-error')
|
|
@@ -17,6 +19,32 @@ const FileStoreHandler = {
|
|
|
RETRY_ATTEMPTS: 3,
|
|
RETRY_ATTEMPTS: 3,
|
|
|
|
|
|
|
|
uploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
|
|
uploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
|
|
|
|
|
+ // Look up the history id for the project if we don't have it already
|
|
|
|
|
+ ProjectDetailsHandler.getDetails(projectId, function (err, project) {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ return callback(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ const historyId = project.overleaf?.history?.id
|
|
|
|
|
+ if (!historyId) {
|
|
|
|
|
+ return callback(new OError('missing history id'))
|
|
|
|
|
+ }
|
|
|
|
|
+ FileStoreHandler.uploadFileFromDiskWithHistoryId(
|
|
|
|
|
+ projectId,
|
|
|
|
|
+ historyId,
|
|
|
|
|
+ fileArgs,
|
|
|
|
|
+ fsPath,
|
|
|
|
|
+ callback
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ uploadFileFromDiskWithHistoryId(
|
|
|
|
|
+ projectId,
|
|
|
|
|
+ historyId,
|
|
|
|
|
+ fileArgs,
|
|
|
|
|
+ fsPath,
|
|
|
|
|
+ callback
|
|
|
|
|
+ ) {
|
|
|
fs.lstat(fsPath, function (err, stat) {
|
|
fs.lstat(fsPath, function (err, stat) {
|
|
|
if (err) {
|
|
if (err) {
|
|
|
logger.warn({ err, projectId, fileArgs, fsPath }, 'error stating file')
|
|
logger.warn({ err, projectId, fileArgs, fsPath }, 'error stating file')
|
|
@@ -36,76 +64,93 @@ const FileStoreHandler = {
|
|
|
)
|
|
)
|
|
|
return callback(new Error('can not upload symlink'))
|
|
return callback(new Error('can not upload symlink'))
|
|
|
}
|
|
}
|
|
|
- Async.retry(
|
|
|
|
|
- FileStoreHandler.RETRY_ATTEMPTS,
|
|
|
|
|
- (cb, results) =>
|
|
|
|
|
- FileStoreHandler._doUploadFileFromDisk(
|
|
|
|
|
- projectId,
|
|
|
|
|
- fileArgs,
|
|
|
|
|
- fsPath,
|
|
|
|
|
- cb
|
|
|
|
|
- ),
|
|
|
|
|
- function (err, result) {
|
|
|
|
|
- if (err) {
|
|
|
|
|
- OError.tag(err, 'Error uploading file, retries failed', {
|
|
|
|
|
- projectId,
|
|
|
|
|
- fileArgs,
|
|
|
|
|
- })
|
|
|
|
|
- return callback(err)
|
|
|
|
|
- }
|
|
|
|
|
- callback(err, result.url, result.fileRef)
|
|
|
|
|
|
|
+ FileHashManager.computeHash(fsPath, function (err, hash) {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ return callback(err)
|
|
|
}
|
|
}
|
|
|
- )
|
|
|
|
|
|
|
+ Async.retry(
|
|
|
|
|
+ FileStoreHandler.RETRY_ATTEMPTS,
|
|
|
|
|
+ cb =>
|
|
|
|
|
+ HistoryManager.uploadBlobFromDisk(
|
|
|
|
|
+ historyId,
|
|
|
|
|
+ hash,
|
|
|
|
|
+ stat.size,
|
|
|
|
|
+ fsPath,
|
|
|
|
|
+ cb
|
|
|
|
|
+ ),
|
|
|
|
|
+ function (err) {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ return callback(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ fileArgs = { ...fileArgs, hash }
|
|
|
|
|
+ Async.retry(
|
|
|
|
|
+ FileStoreHandler.RETRY_ATTEMPTS,
|
|
|
|
|
+ (cb, results) =>
|
|
|
|
|
+ FileStoreHandler._doUploadFileFromDisk(
|
|
|
|
|
+ projectId,
|
|
|
|
|
+ fileArgs,
|
|
|
|
|
+ fsPath,
|
|
|
|
|
+ cb
|
|
|
|
|
+ ),
|
|
|
|
|
+ function (err, result) {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ OError.tag(err, 'Error uploading file, retries failed', {
|
|
|
|
|
+ projectId,
|
|
|
|
|
+ fileArgs,
|
|
|
|
|
+ })
|
|
|
|
|
+ return callback(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ callback(err, result.url, result.fileRef)
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
_doUploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
|
|
_doUploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
|
|
|
const callbackOnce = _.once(callback)
|
|
const callbackOnce = _.once(callback)
|
|
|
|
|
|
|
|
- FileHashManager.computeHash(fsPath, function (err, hashValue) {
|
|
|
|
|
- if (err) {
|
|
|
|
|
- return callbackOnce(err)
|
|
|
|
|
|
|
+ const fileRef = new File(fileArgs)
|
|
|
|
|
+ const fileId = fileRef._id
|
|
|
|
|
+ const readStream = fs.createReadStream(fsPath)
|
|
|
|
|
+ readStream.on('error', function (err) {
|
|
|
|
|
+ logger.warn(
|
|
|
|
|
+ { err, projectId, fileId, fsPath },
|
|
|
|
|
+ 'something went wrong on the read stream of uploadFileFromDisk'
|
|
|
|
|
+ )
|
|
|
|
|
+ callbackOnce(err)
|
|
|
|
|
+ })
|
|
|
|
|
+ readStream.on('open', function () {
|
|
|
|
|
+ const url = FileStoreHandler._buildUrl(projectId, fileId)
|
|
|
|
|
+ const opts = {
|
|
|
|
|
+ method: 'post',
|
|
|
|
|
+ uri: url,
|
|
|
|
|
+ timeout: FIVE_MINS_IN_MS,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'X-File-Hash-From-Web': fileArgs.hash,
|
|
|
|
|
+ }, // send the hash to the filestore as a custom header so it can be checked
|
|
|
}
|
|
}
|
|
|
- const fileRef = new File(Object.assign({}, fileArgs, { hash: hashValue }))
|
|
|
|
|
- const fileId = fileRef._id
|
|
|
|
|
- const readStream = fs.createReadStream(fsPath)
|
|
|
|
|
- readStream.on('error', function (err) {
|
|
|
|
|
|
|
+ const writeStream = request(opts)
|
|
|
|
|
+ writeStream.on('error', function (err) {
|
|
|
logger.warn(
|
|
logger.warn(
|
|
|
{ err, projectId, fileId, fsPath },
|
|
{ err, projectId, fileId, fsPath },
|
|
|
- 'something went wrong on the read stream of uploadFileFromDisk'
|
|
|
|
|
|
|
+ 'something went wrong on the write stream of uploadFileFromDisk'
|
|
|
)
|
|
)
|
|
|
callbackOnce(err)
|
|
callbackOnce(err)
|
|
|
})
|
|
})
|
|
|
- readStream.on('open', function () {
|
|
|
|
|
- const url = FileStoreHandler._buildUrl(projectId, fileId)
|
|
|
|
|
- const opts = {
|
|
|
|
|
- method: 'post',
|
|
|
|
|
- uri: url,
|
|
|
|
|
- timeout: FIVE_MINS_IN_MS,
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'X-File-Hash-From-Web': hashValue,
|
|
|
|
|
- }, // send the hash to the filestore as a custom header so it can be checked
|
|
|
|
|
- }
|
|
|
|
|
- const writeStream = request(opts)
|
|
|
|
|
- writeStream.on('error', function (err) {
|
|
|
|
|
- logger.warn(
|
|
|
|
|
- { err, projectId, fileId, fsPath },
|
|
|
|
|
- 'something went wrong on the write stream of uploadFileFromDisk'
|
|
|
|
|
|
|
+ writeStream.on('response', function (response) {
|
|
|
|
|
+ if (![200, 201].includes(response.statusCode)) {
|
|
|
|
|
+ const err = new OError(
|
|
|
|
|
+ `non-ok response from filestore for upload: ${response.statusCode}`,
|
|
|
|
|
+ { statusCode: response.statusCode }
|
|
|
)
|
|
)
|
|
|
- callbackOnce(err)
|
|
|
|
|
- })
|
|
|
|
|
- writeStream.on('response', function (response) {
|
|
|
|
|
- if (![200, 201].includes(response.statusCode)) {
|
|
|
|
|
- err = new OError(
|
|
|
|
|
- `non-ok response from filestore for upload: ${response.statusCode}`,
|
|
|
|
|
- { statusCode: response.statusCode }
|
|
|
|
|
- )
|
|
|
|
|
- return callbackOnce(err)
|
|
|
|
|
- }
|
|
|
|
|
- callbackOnce(null, { url, fileRef })
|
|
|
|
|
- }) // have to pass back an object because async.retry only accepts a single result argument
|
|
|
|
|
- readStream.pipe(writeStream)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ return callbackOnce(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ callbackOnce(null, { url, fileRef })
|
|
|
|
|
+ }) // have to pass back an object because async.retry only accepts a single result argument
|
|
|
|
|
+ readStream.pipe(writeStream)
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -258,5 +303,6 @@ module.exports = FileStoreHandler
|
|
|
module.exports.promises = promisifyAll(FileStoreHandler, {
|
|
module.exports.promises = promisifyAll(FileStoreHandler, {
|
|
|
multiResult: {
|
|
multiResult: {
|
|
|
uploadFileFromDisk: ['url', 'fileRef'],
|
|
uploadFileFromDisk: ['url', 'fileRef'],
|
|
|
|
|
+ uploadFileFromDiskWithHistoryId: ['url', 'fileRef'],
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|