2
0

attachment.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import type { IAttachment, Ref } from '@growi/core/dist/interfaces';
  2. import type { HydratedDocument } from 'mongoose';
  3. import loggerFactory from '~/utils/logger';
  4. import type Crowi from '../crowi';
  5. import { AttachmentType } from '../interfaces/attachment';
  6. import type { IAttachmentDocument } from '../models/attachment';
  7. import { Attachment } from '../models/attachment';
  8. const fs = require('fs');
  9. const mongoose = require('mongoose');
  10. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  11. const logger = loggerFactory('growi:service:AttachmentService');
  12. const createReadStream = (filePath) => {
  13. return fs.createReadStream(filePath, {
  14. flags: 'r', encoding: null, fd: null, mode: '0666', autoClose: true,
  15. });
  16. };
  17. type AttachHandler = (pageId: string | null, attachment: IAttachmentDocument, file: Express.Multer.File) => Promise<void>;
  18. type DetachHandler = (attachmentId: string) => Promise<void>;
  19. type IAttachmentService = {
  20. createAttachment(
  21. file: Express.Multer.File, user: any, pageId: string | null, attachmentType: AttachmentType,
  22. disposeTmpFileCallback?: (file: Express.Multer.File) => void,
  23. ): Promise<IAttachmentDocument>;
  24. removeAllAttachments(attachments: IAttachmentDocument[]): Promise<void>;
  25. removeAttachment(attachmentId: Ref<IAttachment> | undefined): Promise<void>;
  26. isBrandLogoExist(): Promise<boolean>;
  27. addAttachHandler(handler: AttachHandler): void;
  28. addDetachHandler(handler: DetachHandler): void;
  29. };
  30. /**
  31. * the service class for Attachment and file-uploader
  32. */
  33. export class AttachmentService implements IAttachmentService {
  34. attachHandlers: AttachHandler[] = [];
  35. detachHandlers: DetachHandler[] = [];
  36. crowi: Crowi;
  37. constructor(crowi: Crowi) {
  38. this.crowi = crowi;
  39. }
  40. async createAttachment(file, user, pageId: string | null | undefined = null, attachmentType, disposeTmpFileCallback): Promise<IAttachmentDocument> {
  41. const { fileUploadService } = this.crowi;
  42. // check limit
  43. const res = await fileUploadService.checkLimit(file.size);
  44. if (!res.isUploadable) {
  45. throw new Error(res.errorMessage);
  46. }
  47. // create an Attachment document and upload file
  48. let attachment;
  49. let readStreamForCreateAttachmentDocument;
  50. try {
  51. readStreamForCreateAttachmentDocument = createReadStream(file.path);
  52. attachment = Attachment.createWithoutSave(pageId, user, file.originalname, file.mimetype, file.size, attachmentType);
  53. await fileUploadService.uploadAttachment(readStreamForCreateAttachmentDocument, attachment);
  54. await attachment.save();
  55. const attachHandlerPromises = this.attachHandlers.map((handler) => {
  56. return handler(pageId, attachment, file);
  57. });
  58. // Do not await, run in background
  59. Promise.all(attachHandlerPromises)
  60. .catch((err) => {
  61. logger.error('Error while executing attach handler', err);
  62. })
  63. .finally(() => {
  64. disposeTmpFileCallback?.(file);
  65. });
  66. }
  67. catch (err) {
  68. logger.error('Error while creating attachment', err);
  69. disposeTmpFileCallback?.(file);
  70. throw err;
  71. }
  72. finally {
  73. readStreamForCreateAttachmentDocument.destroy();
  74. }
  75. return attachment;
  76. }
  77. async removeAllAttachments(attachments: HydratedDocument<IAttachmentDocument>[]): Promise<void> {
  78. const { fileUploadService } = this.crowi;
  79. const attachmentsCollection = mongoose.connection.collection('attachments');
  80. const unorderAttachmentsBulkOp = attachmentsCollection.initializeUnorderedBulkOp();
  81. if (attachments.length === 0) {
  82. return;
  83. }
  84. attachments.forEach((attachment) => {
  85. unorderAttachmentsBulkOp.find({ _id: attachment._id }).delete();
  86. });
  87. await unorderAttachmentsBulkOp.execute();
  88. fileUploadService.deleteFiles(attachments);
  89. return;
  90. }
  91. async removeAttachment(attachmentId: Ref<IAttachment> | undefined): Promise<void> {
  92. const { fileUploadService } = this.crowi;
  93. const attachment = await Attachment.findById(attachmentId);
  94. if (attachment == null) {
  95. throw new Error(`Attachment not found: ${attachmentId}`);
  96. }
  97. await fileUploadService.deleteFile(attachment);
  98. await attachment.remove();
  99. const detachedHandlerPromises = this.detachHandlers.map((handler) => {
  100. return handler(attachment._id);
  101. });
  102. // Do not await, run in background
  103. Promise.all(detachedHandlerPromises)
  104. .catch((err) => {
  105. logger.error('Error while executing detached handler', err);
  106. });
  107. return;
  108. }
  109. async isBrandLogoExist(): Promise<boolean> {
  110. const query = { attachmentType: AttachmentType.BRAND_LOGO };
  111. const count = await Attachment.countDocuments(query);
  112. return count >= 1;
  113. }
  114. /**
  115. * Register a handler that will be called after attachment creation
  116. * @param {(pageId: string, attachment: Attachment, file: Express.Multer.File) => Promise<void>} handler
  117. */
  118. addAttachHandler(handler: AttachHandler): void {
  119. this.attachHandlers.push(handler);
  120. }
  121. /**
  122. * Register a handler that will be called before attachment deletion
  123. * @param {(attachmentId: string) => Promise<void>} handler
  124. */
  125. addDetachHandler(handler: DetachHandler): void {
  126. this.detachHandlers.push(handler);
  127. }
  128. }