overleaf/services/web/app/src/Features/FileStore/FileStoreHandler.js
Timothée Alby e73a5d9103 Merge pull request #1923 from overleaf/ta-error-log-level
Reduce Log Level

GitOrigin-RevId: 63a8859e6b78cb0774a28069089f4bce73eb91ec
2019-07-01 14:06:36 +00:00

237 lines
7.2 KiB
JavaScript

const _ = require('underscore')
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')
const Errors = require('../Errors/Errors')
const ONE_MIN_IN_MS = 60 * 1000
const FIVE_MINS_IN_MS = ONE_MIN_IN_MS * 5
const FileStoreHandler = {
RETRY_ATTEMPTS: 3,
uploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
fs.lstat(fsPath, function(err, stat) {
if (err) {
logger.warn({ err, projectId, fileArgs, fsPath }, 'error stating file')
callback(err)
}
if (!stat) {
logger.warn(
{ projectId, fileArgs, fsPath },
'stat is not available, can not check file from disk'
)
return callback(new Error('error getting stat, not available'))
}
if (!stat.isFile()) {
logger.log(
{ projectId, fileArgs, fsPath },
'tried to upload symlink, not continuing'
)
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) {
logger.warn(
{ err, projectId, fileArgs },
'Error uploading file, retries failed'
)
return callback(err)
}
callback(err, result.url, result.fileRef)
}
)
})
},
_doUploadFileFromDisk(projectId, fileArgs, fsPath, callback) {
const callbackOnce = _.once(callback)
FileHashManager.computeHash(fsPath, function(err, hashValue) {
if (err) {
return callbackOnce(err)
}
const fileRef = new File(Object.assign({}, fileArgs, { hash: hashValue }))
const fileId = fileRef._id
logger.log(
{ projectId, fileId, fsPath, hash: hashValue, fileRef },
'uploading file from disk'
)
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': 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'
)
callbackOnce(err)
})
writeStream.on('response', function(response) {
if (![200, 201].includes(response.statusCode)) {
err = new Error(
`non-ok response from filestore for upload: ${
response.statusCode
}`
)
logger.warn(
{ err, statusCode: response.statusCode },
'error uploading to filestore'
)
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)
})
})
},
getFileStream(projectId, fileId, query, callback) {
logger.log(
{ projectId, fileId, query },
'getting file stream from file store'
)
let queryString = ''
if (query != null && query['format'] != null) {
queryString = `?format=${query['format']}`
}
const opts = {
method: 'get',
uri: `${this._buildUrl(projectId, fileId)}${queryString}`,
timeout: FIVE_MINS_IN_MS,
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(
{ err, projectId, fileId, query, opts },
'error in file stream'
)
)
return callback(null, readStream)
},
getFileSize(projectId, fileId, callback) {
const url = this._buildUrl(projectId, fileId)
request.head(url, (err, res) => {
if (err) {
logger.warn(
{ 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.warn(
{ 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')
const opts = {
method: 'delete',
uri: this._buildUrl(projectId, fileId),
timeout: FIVE_MINS_IN_MS
}
return request(opts, function(err, response) {
if (err) {
logger.warn(
{ err, projectId, fileId },
'something went wrong deleting file from filestore'
)
}
return callback(err)
})
},
copyFile(oldProjectId, oldFileId, newProjectId, newFileId, callback) {
logger.log(
{ oldProjectId, oldFileId, newProjectId, newFileId },
'telling filestore to copy a file'
)
const opts = {
method: 'put',
json: {
source: {
project_id: oldProjectId,
file_id: oldFileId
}
},
uri: this._buildUrl(newProjectId, newFileId),
timeout: FIVE_MINS_IN_MS
}
return request(opts, function(err, response) {
if (err) {
logger.warn(
{ err, oldProjectId, oldFileId, newProjectId, newFileId },
'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.warn(
{ uri: opts.uri, statusCode: response.statusCode },
'error uploading to filestore'
)
return callback(err)
}
})
},
_buildUrl(projectId, fileId) {
return `${settings.apis.filestore.url}/project/${projectId}/file/${fileId}`
}
}
module.exports = FileStoreHandler