aws.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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.deleteFiles = async function(attachments) {
  97. if (!this.getIsUploadable()) {
  98. throw new Error('AWS is not configured.');
  99. }
  100. const s3 = S3Factory();
  101. const awsConfig = getAwsConfig();
  102. const filePaths = attachments.map((attachment) => {
  103. return { Key: getFilePathOnStorage(attachment) };
  104. });
  105. const totalParams = {
  106. Bucket: awsConfig.bucket,
  107. Delete: { Objects: filePaths },
  108. };
  109. return s3.deleteObjects(totalParams).promise();
  110. };
  111. lib.deleteFileByFilePath = async function(filePath) {
  112. if (!this.getIsUploadable()) {
  113. throw new Error('AWS is not configured.');
  114. }
  115. const s3 = S3Factory();
  116. const awsConfig = getAwsConfig();
  117. const params = {
  118. Bucket: awsConfig.bucket,
  119. Key: filePath,
  120. };
  121. // check file exists
  122. const isExists = await isFileExists(s3, params);
  123. if (!isExists) {
  124. logger.warn(`Any object that relate to the Attachment (${filePath}) does not exist in AWS S3`);
  125. return;
  126. }
  127. return s3.deleteObject(params).promise();
  128. };
  129. lib.uploadFile = function(fileStream, attachment) {
  130. if (!this.getIsUploadable()) {
  131. throw new Error('AWS is not configured.');
  132. }
  133. logger.debug(`File uploading: fileName=${attachment.fileName}`);
  134. const s3 = S3Factory();
  135. const awsConfig = getAwsConfig();
  136. const filePath = getFilePathOnStorage(attachment);
  137. const params = {
  138. Bucket: awsConfig.bucket,
  139. ContentType: attachment.fileFormat,
  140. Key: filePath,
  141. Body: fileStream,
  142. ACL: 'public-read',
  143. };
  144. return s3.upload(params).promise();
  145. };
  146. /**
  147. * Find data substance
  148. *
  149. * @param {Attachment} attachment
  150. * @return {stream.Readable} readable stream
  151. */
  152. lib.findDeliveryFile = async function(attachment) {
  153. if (!this.getIsReadable()) {
  154. throw new Error('AWS is not configured.');
  155. }
  156. const s3 = S3Factory();
  157. const awsConfig = getAwsConfig();
  158. const filePath = getFilePathOnStorage(attachment);
  159. const params = {
  160. Bucket: awsConfig.bucket,
  161. Key: filePath,
  162. };
  163. // check file exists
  164. const isExists = await isFileExists(s3, params);
  165. if (!isExists) {
  166. throw new Error(`Any object that relate to the Attachment (${filePath}) does not exist in AWS S3`);
  167. }
  168. let stream;
  169. try {
  170. stream = s3.getObject(params).createReadStream();
  171. }
  172. catch (err) {
  173. logger.error(err);
  174. throw new Error(`Coudn't get file from AWS for the Attachment (${attachment._id.toString()})`);
  175. }
  176. // return stream.Readable
  177. return stream;
  178. };
  179. /**
  180. * check the file size limit
  181. *
  182. * In detail, the followings are checked.
  183. * - per-file size limit (specified by MAX_FILE_SIZE)
  184. */
  185. lib.checkLimit = async(uploadFileSize) => {
  186. const maxFileSize = crowi.configManager.getConfig('crowi', 'app:maxFileSize');
  187. const totalLimit = crowi.configManager.getConfig('crowi', 'app:fileUploadTotalLimit');
  188. return lib.doCheckLimit(uploadFileSize, maxFileSize, totalLimit);
  189. };
  190. return lib;
  191. };