2019-06-20 07:17:55 -04:00
|
|
|
const _ = require('underscore')
|
2019-05-29 05:21:06 -04:00
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
const fs = require('fs')
|
|
|
|
const request = require('request')
|
|
|
|
const settings = require('settings-sharelatex')
|
|
|
|
const Async = require('async')
|
|
|
|
const FileHashManager = require('./FileHashManager')
|
|
|
|
const { File } = require('../../models/File')
|
2019-06-20 07:17:55 -04:00
|
|
|
const Errors = require('../Errors/Errors')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-06-20 07:17:55 -04:00
|
|
|
const ONE_MIN_IN_MS = 60 * 1000
|
|
|
|
const FIVE_MINS_IN_MS = ONE_MIN_IN_MS * 5
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-06-20 07:17:55 -04:00
|
|
|
const FileStoreHandler = {
|
2019-05-29 05:21:06 -04:00
|
|
|
RETRY_ATTEMPTS: 3,
|
|
|
|
|
2019-06-20 07:17:55 -04:00
|
|
|
uploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
|
|
|
|
fs.lstat(fsPath, function(err, stat) {
|
|
|
|
if (err) {
|
|
|
|
logger.err({ err, projectId, fileArgs, fsPath }, 'error stating file')
|
2019-05-29 05:21:06 -04:00
|
|
|
callback(err)
|
|
|
|
}
|
2019-06-20 07:17:55 -04:00
|
|
|
if (!stat) {
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.err(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ projectId, fileArgs, fsPath },
|
2019-05-29 05:21:06 -04:00
|
|
|
'stat is not available, can not check file from disk'
|
|
|
|
)
|
|
|
|
return callback(new Error('error getting stat, not available'))
|
|
|
|
}
|
|
|
|
if (!stat.isFile()) {
|
|
|
|
logger.log(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ projectId, fileArgs, fsPath },
|
|
|
|
'tried to upload symlink, not continuing'
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
return callback(new Error('can not upload symlink'))
|
|
|
|
}
|
2019-06-20 07:17:55 -04:00
|
|
|
Async.retry(
|
2019-05-29 05:21:06 -04:00
|
|
|
FileStoreHandler.RETRY_ATTEMPTS,
|
|
|
|
(cb, results) =>
|
|
|
|
FileStoreHandler._doUploadFileFromDisk(
|
2019-06-20 07:17:55 -04:00
|
|
|
projectId,
|
|
|
|
fileArgs,
|
2019-05-29 05:21:06 -04:00
|
|
|
fsPath,
|
|
|
|
cb
|
|
|
|
),
|
|
|
|
function(err, result) {
|
2019-06-20 07:17:55 -04:00
|
|
|
if (err) {
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.err(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ err, projectId, fileArgs },
|
2019-05-29 05:21:06 -04:00
|
|
|
'Error uploading file, retries failed'
|
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
}
|
2019-06-20 07:17:55 -04:00
|
|
|
callback(err, result.url, result.fileRef)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-06-20 07:17:55 -04:00
|
|
|
_doUploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
|
|
|
|
const callbackOnce = _.once(callback)
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-06-20 07:17:55 -04:00
|
|
|
FileHashManager.computeHash(fsPath, function(err, hashValue) {
|
|
|
|
if (err) {
|
|
|
|
return callbackOnce(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-06-20 07:17:55 -04:00
|
|
|
const fileRef = new File(Object.assign({}, fileArgs, { hash: hashValue }))
|
|
|
|
const fileId = fileRef._id
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.log(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ projectId, fileId, fsPath, hash: hashValue, fileRef },
|
2019-05-29 05:21:06 -04:00
|
|
|
'uploading file from disk'
|
|
|
|
)
|
|
|
|
const readStream = fs.createReadStream(fsPath)
|
|
|
|
readStream.on('error', function(err) {
|
|
|
|
logger.err(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ err, projectId, fileId, fsPath },
|
2019-05-29 05:21:06 -04:00
|
|
|
'something went wrong on the read stream of uploadFileFromDisk'
|
|
|
|
)
|
2019-06-20 07:17:55 -04:00
|
|
|
callbackOnce(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
2019-06-20 07:17:55 -04:00
|
|
|
readStream.on('open', function() {
|
|
|
|
const url = FileStoreHandler._buildUrl(projectId, fileId)
|
2019-05-29 05:21:06 -04:00
|
|
|
const opts = {
|
|
|
|
method: 'post',
|
|
|
|
uri: url,
|
2019-06-20 07:17:55 -04:00
|
|
|
timeout: FIVE_MINS_IN_MS,
|
2019-05-29 05:21:06 -04:00
|
|
|
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.err(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ err, projectId, fileId, fsPath },
|
2019-05-29 05:21:06 -04:00
|
|
|
'something went wrong on the write stream of uploadFileFromDisk'
|
|
|
|
)
|
2019-06-20 07:17:55 -04:00
|
|
|
callbackOnce(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
writeStream.on('response', function(response) {
|
|
|
|
if (![200, 201].includes(response.statusCode)) {
|
|
|
|
err = new Error(
|
|
|
|
`non-ok response from filestore for upload: ${
|
|
|
|
response.statusCode
|
|
|
|
}`
|
|
|
|
)
|
|
|
|
logger.err(
|
|
|
|
{ err, statusCode: response.statusCode },
|
|
|
|
'error uploading to filestore'
|
|
|
|
)
|
2019-06-20 07:17:55 -04:00
|
|
|
return callbackOnce(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-06-20 07:17:55 -04:00
|
|
|
callbackOnce(null, { url, fileRef })
|
2019-05-29 05:21:06 -04:00
|
|
|
}) // have to pass back an object because async.retry only accepts a single result argument
|
2019-06-20 07:17:55 -04:00
|
|
|
readStream.pipe(writeStream)
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-06-20 07:17:55 -04:00
|
|
|
getFileStream(projectId, fileId, query, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.log(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ projectId, fileId, query },
|
2019-05-29 05:21:06 -04:00
|
|
|
'getting file stream from file store'
|
|
|
|
)
|
|
|
|
let queryString = ''
|
|
|
|
if (query != null && query['format'] != null) {
|
|
|
|
queryString = `?format=${query['format']}`
|
|
|
|
}
|
|
|
|
const opts = {
|
|
|
|
method: 'get',
|
2019-06-20 07:17:55 -04:00
|
|
|
uri: `${this._buildUrl(projectId, fileId)}${queryString}`,
|
|
|
|
timeout: FIVE_MINS_IN_MS,
|
2019-05-29 05:21:06 -04:00
|
|
|
headers: {}
|
|
|
|
}
|
|
|
|
if (query != null && query['range'] != null) {
|
|
|
|
const rangeText = query['range']
|
|
|
|
if (rangeText && rangeText.match != null && rangeText.match(/\d+-\d+/)) {
|
|
|
|
opts.headers['range'] = `bytes=${query['range']}`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const readStream = request(opts)
|
|
|
|
readStream.on('error', err =>
|
|
|
|
logger.err(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ err, projectId, fileId, query, opts },
|
2019-05-29 05:21:06 -04:00
|
|
|
'error in file stream'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return callback(null, readStream)
|
|
|
|
},
|
|
|
|
|
2019-06-20 07:17:55 -04:00
|
|
|
getFileSize(projectId, fileId, callback) {
|
|
|
|
const url = this._buildUrl(projectId, fileId)
|
|
|
|
request.head(url, (err, res) => {
|
|
|
|
if (err) {
|
|
|
|
logger.err(
|
|
|
|
{ err, projectId, fileId },
|
|
|
|
'failed to get file size from filestore'
|
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
if (res.statusCode === 404) {
|
|
|
|
return callback(new Errors.NotFoundError('file not found in filestore'))
|
|
|
|
}
|
|
|
|
if (res.statusCode !== 200) {
|
|
|
|
logger.err(
|
|
|
|
{ projectId, fileId, statusCode: res.statusCode },
|
|
|
|
'filestore returned non-200 response'
|
|
|
|
)
|
|
|
|
return callback(new Error('filestore returned non-200 response'))
|
|
|
|
}
|
|
|
|
const fileSize = res.headers['content-length']
|
|
|
|
callback(null, fileSize)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
deleteFile(projectId, fileId, callback) {
|
|
|
|
logger.log({ projectId, fileId }, 'telling file store to delete file')
|
2019-05-29 05:21:06 -04:00
|
|
|
const opts = {
|
|
|
|
method: 'delete',
|
2019-06-20 07:17:55 -04:00
|
|
|
uri: this._buildUrl(projectId, fileId),
|
|
|
|
timeout: FIVE_MINS_IN_MS
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
return request(opts, function(err, response) {
|
2019-06-20 07:17:55 -04:00
|
|
|
if (err) {
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.err(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ err, projectId, fileId },
|
2019-05-29 05:21:06 -04:00
|
|
|
'something went wrong deleting file from filestore'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return callback(err)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-06-20 07:17:55 -04:00
|
|
|
copyFile(oldProjectId, oldFileId, newProjectId, newFileId, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.log(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ oldProjectId, oldFileId, newProjectId, newFileId },
|
2019-05-29 05:21:06 -04:00
|
|
|
'telling filestore to copy a file'
|
|
|
|
)
|
|
|
|
const opts = {
|
|
|
|
method: 'put',
|
|
|
|
json: {
|
|
|
|
source: {
|
2019-06-20 07:17:55 -04:00
|
|
|
project_id: oldProjectId,
|
|
|
|
file_id: oldFileId
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
},
|
2019-06-20 07:17:55 -04:00
|
|
|
uri: this._buildUrl(newProjectId, newFileId),
|
|
|
|
timeout: FIVE_MINS_IN_MS
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
return request(opts, function(err, response) {
|
2019-06-20 07:17:55 -04:00
|
|
|
if (err) {
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.err(
|
2019-06-20 07:17:55 -04:00
|
|
|
{ err, oldProjectId, oldFileId, newProjectId, newFileId },
|
2019-05-29 05:21:06 -04:00
|
|
|
'something went wrong telling filestore api to copy file'
|
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
} else if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
|
|
// successful response
|
|
|
|
return callback(null, opts.uri)
|
|
|
|
} else {
|
|
|
|
err = new Error(
|
|
|
|
`non-ok response from filestore for copyFile: ${response.statusCode}`
|
|
|
|
)
|
|
|
|
logger.err(
|
|
|
|
{ uri: opts.uri, statusCode: response.statusCode },
|
|
|
|
'error uploading to filestore'
|
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-06-20 07:17:55 -04:00
|
|
|
_buildUrl(projectId, fileId) {
|
|
|
|
return `${settings.apis.filestore.url}/project/${projectId}/file/${fileId}`
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
2019-06-20 07:17:55 -04:00
|
|
|
|
|
|
|
module.exports = FileStoreHandler
|