attachment.ts 5.3 KB

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