aws.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import {
  2. S3Client, HeadObjectCommand, GetObjectCommand, DeleteObjectsCommand, PutObjectCommand,
  3. } from '@aws-sdk/client-s3';
  4. import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
  5. import urljoin from 'url-join';
  6. import loggerFactory from '~/utils/logger';
  7. const logger = loggerFactory('growi:service:fileUploaderAws');
  8. type AwsCredential = {
  9. accessKeyId: string,
  10. secretAccessKey: string
  11. }
  12. type AwsConfig = {
  13. credentials: AwsCredential,
  14. region: string,
  15. endpoint: string,
  16. bucket: string,
  17. forcePathStyle?: boolean
  18. }
  19. module.exports = (crowi) => {
  20. const Uploader = require('./uploader');
  21. const { configManager } = crowi;
  22. const lib = new Uploader(crowi);
  23. const getAwsConfig = (): AwsConfig => {
  24. return {
  25. credentials: {
  26. accessKeyId: configManager.getConfig('crowi', 'aws:s3AccessKeyId'),
  27. secretAccessKey: configManager.getConfig('crowi', 'aws:s3SecretAccessKey'),
  28. },
  29. region: configManager.getConfig('crowi', 'aws:s3Region'),
  30. endpoint: configManager.getConfig('crowi', 'aws:s3CustomEndpoint'),
  31. bucket: configManager.getConfig('crowi', 'aws:s3Bucket'),
  32. forcePathStyle: configManager.getConfig('crowi', 'aws:s3Bucket') != null,
  33. };
  34. };
  35. const S3Factory = () => {
  36. const config = getAwsConfig();
  37. return new S3Client(config);
  38. };
  39. const getFilePathOnStorage = (attachment) => {
  40. if (attachment.filePath != null) {
  41. return attachment.filePath;
  42. }
  43. const dirName = (attachment.page != null)
  44. ? 'attachment'
  45. : 'user';
  46. const filePath = urljoin(dirName, attachment.fileName);
  47. return filePath;
  48. };
  49. const isFileExists = async(s3, params) => {
  50. try {
  51. await s3.send(new HeadObjectCommand(params));
  52. }
  53. catch (err) {
  54. if (err != null && err.code === 'NotFound') {
  55. return false;
  56. }
  57. throw err;
  58. }
  59. return true;
  60. };
  61. lib.isValidUploadSettings = () => {
  62. return configManager.getConfig('crowi', 'aws:s3AccessKeyId') != null
  63. && configManager.getConfig('crowi', 'aws:s3SecretAccessKey') != null
  64. && (
  65. configManager.getConfig('crowi', 'aws:s3Region') != null
  66. || configManager.getConfig('crowi', 'aws:s3CustomEndpoint') != null
  67. )
  68. && configManager.getConfig('crowi', 'aws:s3Bucket') != null;
  69. };
  70. lib.canRespond = () => {
  71. return !configManager.getConfig('crowi', 'aws:referenceFileWithRelayMode');
  72. };
  73. lib.respond = async(res, attachment) => {
  74. if (!lib.getIsUploadable()) {
  75. throw new Error('AWS is not configured.');
  76. }
  77. const temporaryUrl = attachment.getValidTemporaryUrl();
  78. if (temporaryUrl != null) {
  79. return res.redirect(temporaryUrl);
  80. }
  81. const s3 = S3Factory();
  82. const awsConfig = getAwsConfig();
  83. const filePath = getFilePathOnStorage(attachment);
  84. const lifetimeSecForTemporaryUrl = configManager.getConfig('crowi', 'aws:lifetimeSecForTemporaryUrl');
  85. // issue signed url (default: expires 120 seconds)
  86. // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
  87. const params = {
  88. Bucket: awsConfig.bucket,
  89. Key: filePath,
  90. Expires: lifetimeSecForTemporaryUrl,
  91. };
  92. const signedUrl = await getSignedUrl(s3, new GetObjectCommand(params));
  93. res.redirect(signedUrl);
  94. try {
  95. return attachment.cashTemporaryUrlByProvideSec(signedUrl, lifetimeSecForTemporaryUrl);
  96. }
  97. catch (err) {
  98. logger.error(err);
  99. }
  100. };
  101. lib.deleteFile = async(attachment) => {
  102. const filePath = getFilePathOnStorage(attachment);
  103. return lib.deleteFileByFilePath(filePath);
  104. };
  105. lib.deleteFiles = async(attachments) => {
  106. if (!lib.getIsUploadable()) {
  107. throw new Error('AWS is not configured.');
  108. }
  109. const s3 = S3Factory();
  110. const awsConfig = getAwsConfig();
  111. const filePaths = attachments.map((attachment) => {
  112. return { Key: getFilePathOnStorage(attachment) };
  113. });
  114. const totalParams = {
  115. Bucket: awsConfig.bucket,
  116. Delete: { Objects: filePaths },
  117. };
  118. await s3.send(new DeleteObjectsCommand(totalParams));
  119. };
  120. lib.uploadFile = async(fileStream, attachment) => {
  121. if (!lib.getIsUploadable()) {
  122. throw new Error('AWS is not configured.');
  123. }
  124. logger.debug(`File uploading: fileName=${attachment.fileName}`);
  125. const s3 = S3Factory();
  126. const awsConfig = getAwsConfig();
  127. const filePath = getFilePathOnStorage(attachment);
  128. const params = {
  129. Bucket: awsConfig.bucket,
  130. ContentType: attachment.fileFormat,
  131. Key: filePath,
  132. Body: fileStream,
  133. ACL: 'public-read',
  134. };
  135. await s3.send(new PutObjectCommand(params));
  136. };
  137. lib.findDeliveryFile = async(attachment) => {
  138. if (!lib.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.send(new GetObjectCommand(params));
  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. lib.checkLimit = async(uploadFileSize) => {
  165. const maxFileSize = crowi.configManager.getConfig('crowi', 'app:maxFileSize');
  166. const totalLimit = crowi.configManager.getConfig('crowi', 'app:fileUploadTotalLimit');
  167. return lib.doCheckLimit(uploadFileSize, maxFileSize, totalLimit);
  168. };
  169. return lib;
  170. };