aws.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. const logger = require('@alias/logger')('growi:service:fileUploaderAws');
  2. const urljoin = require('url-join');
  3. const aws = require('aws-sdk');
  4. module.exports = function(crowi) {
  5. const Uploader = require('./uploader');
  6. const { configManager } = crowi;
  7. const lib = new Uploader(crowi);
  8. function getAwsConfig() {
  9. return {
  10. accessKeyId: configManager.getConfig('crowi', 'aws:s3AccessKeyId'),
  11. secretAccessKey: configManager.getConfig('crowi', 'aws:s3SecretAccessKey'),
  12. region: configManager.getConfig('crowi', 'aws:s3Region'),
  13. bucket: configManager.getConfig('crowi', 'aws:s3Bucket'),
  14. customEndpoint: configManager.getConfig('crowi', 'aws:s3CustomEndpoint'),
  15. };
  16. }
  17. function S3Factory() {
  18. const awsConfig = getAwsConfig();
  19. aws.config.update({
  20. accessKeyId: awsConfig.accessKeyId,
  21. secretAccessKey: awsConfig.secretAccessKey,
  22. region: awsConfig.region,
  23. s3ForcePathStyle: awsConfig.customEndpoint ? true : undefined,
  24. });
  25. // undefined & null & '' => default endpoint (genuine S3)
  26. return new aws.S3({ endpoint: awsConfig.customEndpoint || undefined });
  27. }
  28. function getFilePathOnStorage(attachment) {
  29. if (attachment.filePath != null) { // backward compatibility for v3.3.x or below
  30. return attachment.filePath;
  31. }
  32. const dirName = (attachment.page != null)
  33. ? 'attachment'
  34. : 'user';
  35. const filePath = urljoin(dirName, attachment.fileName);
  36. return filePath;
  37. }
  38. async function isFileExists(s3, params) {
  39. // check file exists
  40. try {
  41. await s3.headObject(params).promise();
  42. }
  43. catch (err) {
  44. if (err != null && err.code === 'NotFound') {
  45. return false;
  46. }
  47. // error except for 'NotFound
  48. throw err;
  49. }
  50. return true;
  51. }
  52. lib.isValidUploadSettings = function() {
  53. return this.configManager.getConfig('crowi', 'aws:s3AccessKeyId') != null
  54. && this.configManager.getConfig('crowi', 'aws:s3SecretAccessKey') != null
  55. && (
  56. this.configManager.getConfig('crowi', 'aws:s3Region') != null
  57. || this.configManager.getConfig('crowi', 'aws:s3CustomEndpoint') != null
  58. )
  59. && this.configManager.getConfig('crowi', 'aws:s3Bucket') != null;
  60. };
  61. lib.canRespond = function() {
  62. return !this.configManager.getConfig('crowi', 'aws:referenceFileWithRelayMode');
  63. };
  64. lib.respond = async function(res, attachment) {
  65. if (!this.getIsUploadable()) {
  66. throw new Error('AWS is not configured.');
  67. }
  68. const temporaryUrl = attachment.getValidTemporaryUrl();
  69. if (temporaryUrl != null) {
  70. return res.redirect(temporaryUrl);
  71. }
  72. const s3 = S3Factory();
  73. const awsConfig = getAwsConfig();
  74. const filePath = getFilePathOnStorage(attachment);
  75. const lifetimeSecForTemporaryUrl = this.configManager.getConfig('crowi', 'aws:lifetimeSecForTemporaryUrl');
  76. // issue signed url (default: expires 120 seconds)
  77. // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
  78. const params = {
  79. Bucket: awsConfig.bucket,
  80. Key: filePath,
  81. Expires: lifetimeSecForTemporaryUrl,
  82. };
  83. const signedUrl = s3.getSignedUrl('getObject', params);
  84. res.redirect(signedUrl);
  85. try {
  86. return attachment.cashTemporaryUrlByProvideSec(signedUrl, lifetimeSecForTemporaryUrl);
  87. }
  88. catch (err) {
  89. logger.error(err);
  90. }
  91. };
  92. lib.deleteFile = async function(attachment) {
  93. const filePath = getFilePathOnStorage(attachment);
  94. return lib.deleteFileByFilePath(filePath);
  95. };
  96. lib.deleteFileByFilePath = async function(filePath) {
  97. if (!this.getIsUploadable()) {
  98. throw new Error('AWS is not configured.');
  99. }
  100. const s3 = S3Factory();
  101. const awsConfig = getAwsConfig();
  102. const params = {
  103. Bucket: awsConfig.bucket,
  104. Key: filePath,
  105. };
  106. // check file exists
  107. const isExists = await isFileExists(s3, params);
  108. if (!isExists) {
  109. logger.warn(`Any object that relate to the Attachment (${filePath}) does not exist in AWS S3`);
  110. return;
  111. }
  112. return s3.deleteObject(params).promise();
  113. };
  114. lib.uploadFile = function(fileStream, attachment) {
  115. if (!this.getIsUploadable()) {
  116. throw new Error('AWS is not configured.');
  117. }
  118. logger.debug(`File uploading: fileName=${attachment.fileName}`);
  119. const s3 = S3Factory();
  120. const awsConfig = getAwsConfig();
  121. const filePath = getFilePathOnStorage(attachment);
  122. const params = {
  123. Bucket: awsConfig.bucket,
  124. ContentType: attachment.fileFormat,
  125. Key: filePath,
  126. Body: fileStream,
  127. ACL: 'public-read',
  128. };
  129. return s3.upload(params).promise();
  130. };
  131. /**
  132. * Find data substance
  133. *
  134. * @param {Attachment} attachment
  135. * @return {stream.Readable} readable stream
  136. */
  137. lib.findDeliveryFile = async function(attachment) {
  138. if (!this.getIsReadable()) {
  139. throw new Error('AWS is not configured.');
  140. }
  141. const s3 = S3Factory();
  142. const awsConfig = getAwsConfig();
  143. const filePath = getFilePathOnStorage(attachment);
  144. const params = {
  145. Bucket: awsConfig.bucket,
  146. Key: filePath,
  147. };
  148. // check file exists
  149. const isExists = await isFileExists(s3, params);
  150. if (!isExists) {
  151. throw new Error(`Any object that relate to the Attachment (${filePath}) does not exist in AWS S3`);
  152. }
  153. let stream;
  154. try {
  155. stream = s3.getObject(params).createReadStream();
  156. }
  157. catch (err) {
  158. logger.error(err);
  159. throw new Error(`Coudn't get file from AWS for the Attachment (${attachment._id.toString()})`);
  160. }
  161. // return stream.Readable
  162. return stream;
  163. };
  164. /**
  165. * check the file size limit
  166. *
  167. * In detail, the followings are checked.
  168. * - per-file size limit (specified by MAX_FILE_SIZE)
  169. */
  170. lib.checkLimit = async(uploadFileSize) => {
  171. const maxFileSize = crowi.configManager.getConfig('crowi', 'app:maxFileSize');
  172. const totalLimit = crowi.configManager.getConfig('crowi', 'app:fileUploadTotalLimit');
  173. return lib.doCheckLimit(uploadFileSize, maxFileSize, totalLimit);
  174. };
  175. return lib;
  176. };