const logger = require('@alias/logger')('growi:service:fileUploaderAws'); const urljoin = require('url-join'); const aws = require('aws-sdk'); module.exports = function(crowi) { const Uploader = require('./uploader'); const { configManager } = crowi; const lib = new Uploader(crowi); function getAwsConfig() { return { accessKeyId: configManager.getConfig('crowi', 'aws:s3AccessKeyId'), secretAccessKey: configManager.getConfig('crowi', 'aws:s3SecretAccessKey'), region: configManager.getConfig('crowi', 'aws:s3Region'), bucket: configManager.getConfig('crowi', 'aws:s3Bucket'), customEndpoint: configManager.getConfig('crowi', 'aws:s3CustomEndpoint'), }; } function S3Factory() { const awsConfig = getAwsConfig(); aws.config.update({ accessKeyId: awsConfig.accessKeyId, secretAccessKey: awsConfig.secretAccessKey, region: awsConfig.region, s3ForcePathStyle: awsConfig.customEndpoint ? true : undefined, }); // undefined & null & '' => default endpoint (genuine S3) return new aws.S3({ endpoint: awsConfig.customEndpoint || undefined }); } function getFilePathOnStorage(attachment) { if (attachment.filePath != null) { // backward compatibility for v3.3.x or below return attachment.filePath; } const dirName = (attachment.page != null) ? 'attachment' : 'user'; const filePath = urljoin(dirName, attachment.fileName); return filePath; } async function isFileExists(s3, params) { // check file exists try { await s3.headObject(params).promise(); } catch (err) { if (err != null && err.code === 'NotFound') { return false; } // error except for 'NotFound throw err; } return true; } lib.isValidUploadSettings = function() { return this.configManager.getConfig('crowi', 'aws:s3AccessKeyId') != null && this.configManager.getConfig('crowi', 'aws:s3SecretAccessKey') != null && ( this.configManager.getConfig('crowi', 'aws:s3Region') != null || this.configManager.getConfig('crowi', 'aws:s3CustomEndpoint') != null ) && this.configManager.getConfig('crowi', 'aws:s3Bucket') != null; }; lib.canRespond = function() { return !this.configManager.getConfig('crowi', 'aws:referenceFileWithRelayMode'); }; lib.respond = async function(res, attachment) { if (!this.getIsUploadable()) { throw new Error('AWS is not configured.'); } const temporaryUrl = attachment.getValidTemporaryUrl(); if (temporaryUrl != null) { return res.redirect(temporaryUrl); } const s3 = S3Factory(); const awsConfig = getAwsConfig(); const filePath = getFilePathOnStorage(attachment); const lifetimeSecForTemporaryUrl = this.configManager.getConfig('crowi', 'aws:lifetimeSecForTemporaryUrl'); // issue signed url (default: expires 120 seconds) // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property const params = { Bucket: awsConfig.bucket, Key: filePath, Expires: lifetimeSecForTemporaryUrl, }; const signedUrl = s3.getSignedUrl('getObject', params); res.redirect(signedUrl); try { return attachment.cashTemporaryUrlByProvideSec(signedUrl, lifetimeSecForTemporaryUrl); } catch (err) { logger.error(err); } }; lib.deleteFile = async function(attachment) { const filePath = getFilePathOnStorage(attachment); return lib.deleteFileByFilePath(filePath); }; lib.deleteFiles = async function(attachments) { if (!this.getIsUploadable()) { throw new Error('AWS is not configured.'); } const s3 = S3Factory(); const awsConfig = getAwsConfig(); const filePaths = attachments.map((attachment) => { return { Key: getFilePathOnStorage(attachment) }; }); const totalParams = { Bucket: awsConfig.bucket, Delete: { Objects: filePaths }, }; return s3.deleteObjects(totalParams).promise(); }; lib.deleteFileByFilePath = async function(filePath) { if (!this.getIsUploadable()) { throw new Error('AWS is not configured.'); } const s3 = S3Factory(); const awsConfig = getAwsConfig(); const params = { Bucket: awsConfig.bucket, Key: filePath, }; // check file exists const isExists = await isFileExists(s3, params); if (!isExists) { logger.warn(`Any object that relate to the Attachment (${filePath}) does not exist in AWS S3`); return; } return s3.deleteObject(params).promise(); }; lib.uploadFile = function(fileStream, attachment) { if (!this.getIsUploadable()) { throw new Error('AWS is not configured.'); } logger.debug(`File uploading: fileName=${attachment.fileName}`); const s3 = S3Factory(); const awsConfig = getAwsConfig(); const filePath = getFilePathOnStorage(attachment); const params = { Bucket: awsConfig.bucket, ContentType: attachment.fileFormat, Key: filePath, Body: fileStream, ACL: 'public-read', }; return s3.upload(params).promise(); }; /** * Find data substance * * @param {Attachment} attachment * @return {stream.Readable} readable stream */ lib.findDeliveryFile = async function(attachment) { if (!this.getIsReadable()) { throw new Error('AWS is not configured.'); } const s3 = S3Factory(); const awsConfig = getAwsConfig(); const filePath = getFilePathOnStorage(attachment); const params = { Bucket: awsConfig.bucket, Key: filePath, }; // check file exists const isExists = await isFileExists(s3, params); if (!isExists) { throw new Error(`Any object that relate to the Attachment (${filePath}) does not exist in AWS S3`); } let stream; try { stream = s3.getObject(params).createReadStream(); } catch (err) { logger.error(err); throw new Error(`Coudn't get file from AWS for the Attachment (${attachment._id.toString()})`); } // return stream.Readable return stream; }; /** * check the file size limit * * In detail, the followings are checked. * - per-file size limit (specified by MAX_FILE_SIZE) */ lib.checkLimit = async(uploadFileSize) => { const maxFileSize = crowi.configManager.getConfig('crowi', 'app:maxFileSize'); const totalLimit = crowi.configManager.getConfig('crowi', 'app:fileUploadTotalLimit'); return lib.doCheckLimit(uploadFileSize, maxFileSize, totalLimit); }; return lib; };