UNPKG

@amnet/server

Version:

A tweaked version of pubsweet-server for use by Coko's projects

509 lines (439 loc) 12.1 kB
const AWS = require('aws-sdk') const fs = require('fs-extra') const config = require('config') const forEach = require('lodash/forEach') const crypto = require('crypto') const path = require('path') const mime = require('mime-types') const logger = require('@pubsweet/logger') const { getFileExtension, writeFileFromStream, handleUnsupportedImageFormats, handleImageVersionsCreation, convertFileStreamIntoBuffer, getImageFileMetadata, } = require('../helpers') const { tempFolderPath } = config.get('pubsweet-server') const imageConversionToSupportedFormatMapper = { eps: 'svg', } // Initializing Storage Interface let s3 const healthCheck = () => { try { if (!s3) { throw new Error( 's3 does not exist! Probably configuration is missing/invalid', ) } const { bucket } = config.get('fileStorage') return new Promise((resolve, reject) => { s3.getBucketLogging({ Bucket: bucket }, (err, data) => { if (err) { logger.error( 'File Storage Healthcheck: Communication to remote file service unsuccessful', ) return reject(err) } logger.info('File Storage Healthcheck: OK') return resolve(data) }) }) } catch (e) { throw new Error(e) } } const connectToFileStorage = () => { if (!config.has('fileStorage')) { throw new Error( 'You have declared that you will use file storage but fileStorage configuration is missing', ) } const { accessKeyId, secretAccessKey, bucket, protocol, host, port, } = config.get('fileStorage') if (!protocol) { throw new Error( 'Missing required protocol param for initializing file storage', ) } if (!host) { throw new Error('Missing required host param for initializing file storage') } if (!accessKeyId) { throw new Error( 'Missing required accessKeyId param for initializing file storage', ) } if (!accessKeyId) { throw new Error( 'Missing required secretAccessKey param for initializing file storage', ) } if (!bucket) { throw new Error( 'Missing required bucket param for initializing file storage', ) } const serverUrl = `${protocol}://${host}${port ? `:${port}` : ''}` s3 = new AWS.S3({ accessKeyId, signatureVersion: 'v4', secretAccessKey, s3ForcePathStyle: true, endpoint: serverUrl, }) healthCheck() } const getURL = async (objectKey, options = {}) => { if (!s3) { throw new Error( 's3 does not exist! Probably configuration is missing/invalid', ) } const { bucket } = config.get('fileStorage') const { expiresIn } = options const s3Params = { Bucket: bucket, Key: objectKey, Expires: expiresIn || parseInt(86400, 10), // 1 day lease } return s3.getSignedUrl('getObject', s3Params) } const uploadFileHandler = (fileStream, filename, mimetype) => { if (!s3) { throw new Error( 's3 does not exist! Probably configuration is missing/invalid', ) } const { bucket } = config.get('fileStorage') const params = { Bucket: bucket, Key: filename, // file name you want to save as Body: fileStream, ContentType: mimetype, } return new Promise((resolve, reject) => { s3.upload(params, (err, data) => { if (err) { reject(err) } // do we need etag? const { Key } = data resolve({ key: Key }) }) }) } const handleImageUpload = async (fileStream, hashedFilename) => { try { const storedObjects = [] const randomHash = crypto.randomBytes(6).toString('hex') const tempDirRoot = tempFolderPath || path.join(process.cwd(), 'temp') const tempDir = path.join(tempDirRoot, randomHash) let tempSmallFilePath let tempMediumFilePath await fs.ensureDir(tempDir) const originalFilePath = path.join(tempDir, hashedFilename) await writeFileFromStream(fileStream, originalFilePath) /* eslint-disable no-prototype-builtins */ if ( imageConversionToSupportedFormatMapper.hasOwnProperty( getFileExtension(hashedFilename), ) ) { await handleUnsupportedImageFormats(hashedFilename, tempDir) const { tempSmallFile, tempMediumFile, tempOriginalFilePath, } = await handleImageVersionsCreation(hashedFilename, tempDir, true) tempSmallFilePath = tempSmallFile tempMediumFilePath = tempMediumFile const originalImageStream = fs.createReadStream(tempOriginalFilePath) const originalFileBuffer = await convertFileStreamIntoBuffer( originalImageStream, ) const { width, height, space, density, size, } = await getImageFileMetadata(originalFileBuffer) const original = await uploadFileHandler( fs.createReadStream(originalFilePath), hashedFilename, mime.lookup(hashedFilename), ) original.imageMetadata = { density, height, space, width, } original.size = size original.extension = `${getFileExtension(hashedFilename)}` original.type = 'original' original.mimetype = mime.lookup(hashedFilename) storedObjects.push(original) } else { const { tempSmallFile, tempMediumFile, } = await handleImageVersionsCreation(hashedFilename, tempDir) tempSmallFilePath = tempSmallFile tempMediumFilePath = tempMediumFile const originalImageStream = fs.createReadStream(originalFilePath) const originalFileBuffer = await convertFileStreamIntoBuffer( originalImageStream, ) const { width, height, space, density, size, } = await getImageFileMetadata(originalFileBuffer) const original = await uploadFileHandler( fs.createReadStream(originalFilePath), hashedFilename, mime.lookup(hashedFilename), ) original.imageMetadata = { density, height, space, width, } original.size = size original.extension = `${getFileExtension(hashedFilename)}` original.type = 'original' original.mimetype = mime.lookup(hashedFilename) storedObjects.push(original) } /* eslint-enable no-prototype-builtins */ const mediumImageStream = fs.createReadStream(tempMediumFilePath) const medium = await uploadFileHandler( fs.createReadStream(tempMediumFilePath), path.basename(tempMediumFilePath), mime.lookup(tempMediumFilePath), ) const mediumFileBuffer = await convertFileStreamIntoBuffer( mediumImageStream, ) const { width: mWidth, height: mHeight, space: mSpace, density: mDensity, size: mSize, } = await getImageFileMetadata(mediumFileBuffer) medium.imageMetadata = { density: mDensity, height: mHeight, space: mSpace, width: mWidth, } medium.size = mSize medium.extension = `${getFileExtension(tempMediumFilePath)}` medium.type = 'medium' medium.mimetype = mime.lookup(tempMediumFilePath) storedObjects.push(medium) const smallImageStream = fs.createReadStream(tempSmallFilePath) const small = await uploadFileHandler( fs.createReadStream(tempSmallFilePath), path.basename(tempSmallFilePath), mime.lookup(tempSmallFilePath), ) const smallFileBuffer = await convertFileStreamIntoBuffer(smallImageStream) const { width: sWidth, height: sHeight, space: sSpace, density: sDensity, size: sSize, } = await getImageFileMetadata(smallFileBuffer) small.imageMetadata = { density: sDensity, height: sHeight, space: sSpace, width: sWidth, } small.size = sSize small.extension = `${getFileExtension(tempSmallFilePath)}` small.type = 'small' small.mimetype = mime.lookup(tempSmallFilePath) storedObjects.push(small) await fs.remove(tempDir) return storedObjects } catch (e) { throw new Error(e) } } // Returns an array of stored objects // [{ // type: String (values: original/small/medium), // key: String // mimetype: String // extension: String // size: Integer // }] const upload = async (fileStream, filename, options = {}) => { try { if (!filename) { throw new Error('filename is required') } const { forceFilenameAsObjectKey } = options const mimetype = mime.lookup(filename) let storedObjects = [] let hashedFilename if (forceFilenameAsObjectKey) { hashedFilename = filename } else { hashedFilename = `${crypto .randomBytes(6) .toString('hex')}${getFileExtension(filename, true)}` } /* eslint-disable no-prototype-builtins */ if ( !mimetype.match(/^image\//) && !imageConversionToSupportedFormatMapper.hasOwnProperty( getFileExtension(filename), ) ) { const storedObject = await uploadFileHandler( fileStream, hashedFilename, mimetype, ) const { ContentLength } = await getFileInfo(storedObject.key) storedObject.type = 'original' storedObject.size = ContentLength storedObject.extension = `${getFileExtension(filename)}` storedObject.mimetype = mimetype storedObjects.push(storedObject) return storedObjects } /* eslint-enable no-prototype-builtins */ storedObjects = await handleImageUpload( fileStream, hashedFilename, mimetype, ) return storedObjects } catch (e) { throw new Error(e) } } const getFileInfo = key => { if (!s3) { throw new Error( 's3 does not exist! Probably configuration is missing/invalid', ) } const { bucket } = config.get('fileStorage') const params = { Bucket: bucket, Key: key, } return new Promise((resolve, reject) => { s3.getObject(params, (err, data) => { if (err) { reject(err) } resolve(data) }) }) } const list = () => { if (!s3) { throw new Error( 's3 does not exist! Probably configuration is missing/invalid', ) } const { bucket } = config.get('fileStorage') const params = { Bucket: bucket, } return new Promise((resolve, reject) => { s3.listObjects(params, (err, data) => { if (err) { reject(err) } resolve(data) }) }) } // Accepts an array of object keys const deleteFiles = objectKeys => { if (!s3) { throw new Error( 's3 does not exist! Probably configuration is missing/invalid', ) } const { bucket } = config.get('fileStorage') if (objectKeys.length === 0) { throw new Error('the provided array of keys if empty') } const params = { Bucket: bucket, Delete: { Objects: [], Quiet: false, }, } forEach(objectKeys, objectKey => { params.Delete.Objects.push({ Key: objectKey }) }) return new Promise((resolve, reject) => { s3.deleteObjects(params, (err, data) => { if (err) { reject(err) } resolve(data) }) }) } const download = (key, localPath) => { if (!s3) { throw new Error( 's3 does not exist! Probably configuration is missing/invalid', ) } const { bucket } = config.get('fileStorage') const fileStream = fs.createWriteStream(localPath) const s3Stream = s3.getObject({ Bucket: bucket, Key: key }).createReadStream() return new Promise((resolve, reject) => { // Listen for errors returned by the service s3Stream.on('error', err => { // NoSuchKey: The specified key does not exist reject(err) }) s3Stream .pipe(fileStream) .on('error', err => { // capture any errors that occur when writing data to the file reject(err) console.error('File Stream:', err) }) .on('close', () => { resolve() }) }) } module.exports = { connectToFileStorage, healthCheck, getURL, upload, deleteFiles, list, download, uploadFileHandler, }